outlook_auto/
application.rs

1use windows::{
2    core::{w, GUID, VARIANT}, 
3    Win32::System::Com::{CLSIDFromProgID, CoCreateInstance, IDispatch, DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPPARAMS}
4};
5
6use crate::{
7    co_initialize, 
8    common::{dispatch::{HasDispatch, Invocation}, 
9    variant::{EvilVariant, TypedVariant, VariantError}}, 
10    WinError, LOCALE_USER_DEFAULT, OBJECT_CONTEXT};
11
12
13pub struct Outlook {
14    pub app : IDispatch,
15    pub namespace : IDispatch,
16}
17
18impl Outlook {
19    pub fn new() -> Result<Self, WinError> {
20        co_initialize();
21    
22        let app : IDispatch = unsafe {
23            let clsid = CLSIDFromProgID(w!("Outlook.Application")).expect("Couldn't get CLSID");
24            CoCreateInstance(&clsid, None, OBJECT_CONTEXT)
25        }.map_err(
26            |e| WinError::Internal(e)
27        )?;
28
29        let params = DISPPARAMS {
30            cArgs : 1,
31            rgvarg : &mut VARIANT::from("MAPI"),
32            ..Default::default()
33        };
34
35        let mut namespace_variant = VARIANT::default();
36 
37        unsafe {
38            app.Invoke(
39                272,
40                &GUID::zeroed(),
41                LOCALE_USER_DEFAULT,
42                DISPATCH_METHOD,
43                &params,
44                Some(&mut namespace_variant),
45                None,
46                None,
47            )
48        }.unwrap();
49
50        let namespace = IDispatch::try_from(&namespace_variant).expect("couldnt cast VARIANT to IDispatch");
51
52        Ok(Outlook {
53            app,
54            namespace
55        })
56    }
57
58    // Root folder, first name on path, should be the base address in Outlook
59    pub fn get_folder(&self, path_to_folder : Vec<&str>) -> Result<Option<Folder>, WinError> {
60        let mut folder_names = path_to_folder.into_iter();
61
62        let top_folder_name = folder_names.next().expect("Folder chain cannot be empty");
63        let mut subfolder = match Folder(self.namespace.clone()).get_subfolder(top_folder_name)? {
64            None => return Ok(None),
65            Some(folder) => folder,
66        };
67
68        for subfolder_name in folder_names {
69            subfolder = match subfolder.get_subfolder(subfolder_name)? {
70                None => return Ok(None),
71                Some(folder) => folder,
72            };
73        };  
74        Ok(Some(subfolder))
75    }
76}
77
78impl HasDispatch for Outlook {
79    fn dispatch(&self) -> &IDispatch {
80        &self.app
81    }
82}
83
84#[derive(Clone)]
85pub struct Folder(pub IDispatch);
86
87impl Folder {
88    pub fn get_subfolder(&self, folder_name : &str) -> Result<Option<Folder>, WinError> {
89        let params = DISPPARAMS {
90            cArgs : 1,
91            rgvarg : &mut VARIANT::from(folder_name),
92            ..Default::default()
93        };
94
95        let dispid = self.get_dispid("Folders")?;
96        let mut folder = VARIANT::default();
97
98        unsafe {
99            self.0.Invoke(
100                dispid,
101                &GUID::zeroed(),
102                LOCALE_USER_DEFAULT,
103                DISPATCH_PROPERTYGET,
104                &params,
105                Some(&mut folder),
106                None,
107                None,
108            ).map_err(|e| WinError::Internal(e))?
109        }
110
111        return Ok(Some(Folder(IDispatch::try_from(&folder).expect("couldnt cast VARIANT to folder dispatch"))));
112    }
113
114    pub(crate) fn subfolder_names(&self) -> Result<Vec<String>, WinError> {
115        let mut foldernames = vec![];
116        let subfolders = match self.prop("Folders") {
117            Ok(TypedVariant::Dispatch(d)) => d,
118            Ok(result) => return Err(WinError::VariantError(VariantError::Mismatch { method: "Folders".to_string(), result })),
119            Err(e) => return Err(e),
120        };
121
122        let Ok(TypedVariant::Dispatch(first_folder)) = subfolders.call("GetFirst", Invocation::Method, None) else {
123            return Ok(foldernames);
124        };
125
126        match first_folder.prop("Name")? {
127            TypedVariant::Bstr(name) => foldernames.push(name.to_string()),
128            result => return Err(WinError::VariantError(VariantError::Mismatch {method : "Name".to_string(), result})),
129        };
130
131        loop {
132            match subfolders.call("GetNext", Invocation::Method, None) {
133                Ok(TypedVariant::Dispatch(subfolder)) => {
134                    match subfolder.prop("Name")? {
135                        TypedVariant::Bstr(name) => foldernames.push(name.to_string()),
136                        result => return Err(WinError::VariantError(VariantError::Mismatch {method : "Name".to_string(), result})),
137                    }
138                },
139                Ok(result) => {
140                    return Err(WinError::VariantError(VariantError::Mismatch {method : "Name".to_string(), result}))
141                }
142                Err(WinError::VariantError(VariantError::NullPointer)) => break, // "Iterator" exhausted
143                Err(e) => return Err(e),
144            }
145        }
146        Ok(foldernames)
147    }
148
149    pub fn count(&self) -> Option<usize> {
150        let TypedVariant::Dispatch(items) = self.prop("Items").expect("Folder should have Items property") else {
151            return None
152        };
153
154        match items.prop("Count") {
155            Ok(TypedVariant::Int32(count)) => Some(count as usize),
156            Err(WinError::VariantError(VariantError::NullPointer)) => return Some(0),
157            _ => None,
158        }
159    }
160
161    pub fn emails(&self) -> Result<Vec<MailItem>, WinError> {
162        let mut items = Vec::with_capacity(self.count().unwrap_or(0));
163
164        items.extend(self.iter()?);
165        Ok(items)
166    }
167
168    fn iter(&self) -> Result<MailItemIterator, WinError> {
169        match self.prop("Items")? {
170            TypedVariant::Dispatch(d) => Ok(MailItemIterator(d,true)),
171            result => Err(WinError::VariantError(VariantError::Mismatch { method: "Items".to_string(), result })),
172        }
173    }
174}
175
176impl HasDispatch for Folder {
177    fn dispatch(&self) -> &IDispatch {
178        &self.0
179    }
180}
181
182pub struct MailItemIterator(IDispatch,bool);
183
184impl HasDispatch for MailItemIterator {
185    fn dispatch(&self) -> &IDispatch {
186        &self.0
187    }
188}
189
190impl Iterator for MailItemIterator {
191    type Item = MailItem;
192
193    fn next(&mut self) -> Option<Self::Item> {
194        let method = if self.1 {
195            self.1 = false;
196            "GetFirst"
197        } else {
198            "GetNext"
199        };
200        match self.call_evil(method, Invocation::Method, None) {
201            Ok(EvilVariant { vt : 9, union: 0, .. })  => return None, // End of iterator is nullpointer
202            Ok(evil) if evil.vt == 9 => return Some(MailItem(IDispatch::from(evil))),
203            Ok(result) => panic!("Expected MailItem Dispatch while iterating, found {:?}", result),
204            Err(e) => panic!("MailItem Iterator failed with: {:?}", e),
205        };
206    }
207}
208
209pub struct MailItem(IDispatch);
210
211impl MailItem {
212    /// Moves the MailItem to the given Outlook folder
213    pub fn move_to(&self, target : &Folder) -> Result<(), WinError> {
214        let params = DISPPARAMS {
215            rgvarg : &mut VARIANT::from(target.0.clone()),
216            cArgs : 1,
217            ..Default::default()
218        };
219
220        self.call("Move", Invocation::Method, Some(params))?;
221        
222        Ok(())
223    }
224
225    // String properties
226    fn string_property(&self, name : &str) -> Result<String, WinError> {
227        match self.prop(name)? {
228            TypedVariant::Bstr(string) => Ok(string.to_string()),
229            result => Err(WinError::VariantError(VariantError::Mismatch { method: name.to_string(), result })),
230        }
231    }
232
233    /// Returns email subject line
234    pub fn subject(&self) -> Result<String, WinError> {
235        self.string_property("Subject")
236    }
237
238    /// Returns entire email body as a string
239    pub fn body(&self) -> Result<String, WinError> {
240        self.string_property("Body")
241    }
242
243    pub fn received_time(&self) -> Result<String, WinError> {
244        self.string_property("ReceivedTime")
245    }
246
247    pub fn sender_address(&self) -> Result<String, WinError> {
248        self.string_property("SenderEmailAddress")
249    }
250}
251
252impl HasDispatch for MailItem {
253    fn dispatch(&self) -> &IDispatch {
254        &self.0
255    }
256}