Skip to content

Commit b7ac62a

Browse files
authored
Merge pull request #10 from tutao/send-documents-2
implement MapiSendDocuments
2 parents 94af217 + bdb2bc0 commit b7ac62a

File tree

10 files changed

+659
-62
lines changed

10 files changed

+659
-62
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ jobs:
1313

1414
steps:
1515
- uses: actions/checkout@v2
16+
- name: Check formatting
17+
run: cargo fmt -- --check
18+
- name: Run lints
19+
run: cargo clippy --target "x86_64-pc-windows-msvc" -- -D clippy::all
1620
- name: Run tests
1721
run: cargo test --verbose --target "x86_64-pc-windows-msvc"

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mapirs"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
authors = ["nig <nig@tutao.de>"]
55
edition = "2018"
66

licenses.html

Lines changed: 475 additions & 51 deletions
Large diffs are not rendered by default.

src/environment.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ fn reg_key() -> io::Result<RegKey> {
1717
// let subkey_path_release = "SOFTWARE\\450699d2-1c81-5ee5-aec6-08dddb7af9d7"
1818

1919
// the client saves the path to the executable to hklm/software/Clients/Mail/tutanota/EXEPath
20+
// or hkcu/software/Clients/Mail/tutanota/EXEPath
2021
// that key must be there, otherwise windows couldn't have called this DLL because
2122
// the path to it is next to it under DLLPath.
2223

2324
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
25+
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
26+
let subk = "SOFTWARE\\Clients\\Mail\\tutanota";
2427
// if this fails, the client is not installed correctly or the registry is borked.
25-
hklm.open_subkey("SOFTWARE\\Clients\\Mail\\tutanota")
28+
hkcu.open_subkey(subk).or_else(|_| hklm.open_subkey(subk))
2629
}
2730

2831
/// access the registry to try and get
@@ -38,20 +41,32 @@ pub fn client_path() -> io::Result<OsString> {
3841
#[cfg(not(test))]
3942
fn log_path() -> io::Result<OsString> {
4043
let tutanota_key = reg_key()?;
41-
tutanota_key.get_value("LOGPath")
44+
let log_dir: String = tutanota_key.get_value("LOGPath")?;
45+
replace_profile(log_dir)
4246
}
4347

4448
#[cfg(test)]
4549
fn log_path() -> io::Result<OsString> {
4650
Ok(OsString::from("C:\\some\\weird\\path"))
4751
}
4852

53+
/// replace the %USERPROFILE% placeholder in a String with
54+
/// the value of the USERPROFILE env variable
55+
fn replace_profile(val: String) -> io::Result<OsString> {
56+
let profile =
57+
std::env::var("USERPROFILE").map_err(|_e| io::Error::from(io::ErrorKind::NotFound))?;
58+
Ok(OsString::from(
59+
val.replace("%USERPROFILE%", profile.as_str()),
60+
))
61+
}
62+
4963
/// retrieve the configured tmp dir from the registry and
5064
/// try to ensure the directory is there.
5165
#[cfg(not(test))]
5266
pub fn tmp_path() -> io::Result<OsString> {
5367
let tutanota_key = reg_key()?;
54-
let tmp_dir = tutanota_key.get_value("TMPPath")?;
68+
let tmp_dir: String = tutanota_key.get_value("TMPPath")?;
69+
let tmp_dir = replace_profile(tmp_dir)?;
5570
fs::create_dir_all(&tmp_dir)?;
5671
Ok(tmp_dir)
5772
}
@@ -140,6 +155,8 @@ pub fn current_time_formatted() -> String {
140155

141156
#[cfg(test)]
142157
mod test {
158+
use crate::environment::replace_profile;
159+
143160
#[test]
144161
fn sha_head_works() {
145162
use crate::environment::sha_head;
@@ -150,4 +167,19 @@ mod test {
150167
assert_eq!("e3b0", out);
151168
assert_eq!(4, out.capacity());
152169
}
170+
171+
#[test]
172+
fn replace_profile_works() {
173+
let var = std::env::var("USERPROFILE");
174+
std::env::set_var("USERPROFILE", "C:\\meck");
175+
assert_eq!(
176+
"C:\\meck\\a\\file.txt",
177+
replace_profile("%USERPROFILE%\\a\\file.txt".to_owned()).unwrap()
178+
);
179+
std::env::remove_var("USERPROFILE");
180+
assert!(replace_profile("%USERPROFILE%\\a\\file.txt".to_owned()).is_err());
181+
if var.is_ok() {
182+
std::env::set_var("USERPROFILE", var.unwrap());
183+
}
184+
}
153185
}

src/ffi/conversion.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,50 @@ pub fn copy_c_array_to_vec<T: Clone>(ptr: *const T, count: usize) -> Vec<T> {
8686
slc.to_vec()
8787
}
8888
}
89+
90+
/// MapiSendDocuments gets its file paths as a list packed into a string with
91+
/// a delimiter:
92+
/// C:\a.txt;C:\b.txt;A:\d.jpg
93+
pub fn unpack_strings(packed: String, delim: &str) -> Vec<String> {
94+
match delim {
95+
"" => vec![packed],
96+
_ => packed
97+
.split(delim)
98+
.filter(|s| !s.is_empty())
99+
.map(|s| s.to_owned())
100+
.collect(),
101+
}
102+
}
103+
104+
#[cfg(test)]
105+
mod test {
106+
use crate::ffi::conversion::unpack_strings;
107+
108+
#[test]
109+
fn unpack_strings_works() {
110+
let delim = ";";
111+
assert_eq!(
112+
unpack_strings("A;B;C".to_owned(), delim),
113+
vec!["A", "B", "C"]
114+
);
115+
116+
assert_eq!(unpack_strings("".to_owned(), delim), Vec::<String>::new());
117+
118+
assert_eq!(unpack_strings(";;".to_owned(), delim), Vec::<String>::new());
119+
120+
assert_eq!(
121+
unpack_strings("C:\\a.txt;C:\\b.jpg".to_owned(), ""),
122+
vec!["C:\\a.txt;C:\\b.jpg"]
123+
);
124+
125+
assert_eq!(
126+
unpack_strings("C:\\a.txt;C:\\b.jpg".to_owned(), "%"),
127+
vec!["C:\\a.txt;C:\\b.jpg"]
128+
);
129+
130+
assert_eq!(
131+
unpack_strings(";C:\\a.txt;".to_owned(), delim),
132+
vec!["C:\\a.txt"]
133+
);
134+
}
135+
}

src/ffi/mod.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::convert::TryFrom;
22

33
use crate::commands;
44
use crate::commands::send_mail;
5+
use crate::ffi::conversion::{maybe_string_from_raw_ptr, unpack_strings};
56
use crate::flags::{
67
MapiAddressFlags, MapiDetailsFlags, MapiFindNextFlags, MapiLogonFlags, MapiReadMailFlags,
78
MapiResolveNameFlags, MapiSaveMailFlags, MapiSendMailFlags, MapiStatusCode,
@@ -81,15 +82,35 @@ pub extern "C" fn MAPISendMail(
8182
pub extern "C" fn MAPISendDocuments(
8283
_ui_param: ULongPtr,
8384
// __in LPSTR lpszDelimChar
84-
_delim_char: InLpStr,
85+
delim_char: InLpStr,
8586
// __in LPSTR lpszFilePaths
86-
_file_paths: InLpStr,
87+
file_paths: InLpStr,
8788
// __in LPSTR lpszFileNames
88-
_file_names: InLpStr,
89+
file_names: InLpStr,
8990
_reserved: ULong,
9091
) -> MapiStatusCode {
9192
commands::log_to_file("mapisenddocuments", "");
92-
MapiStatusCode::NotSupported
93+
// some app may put null as delim if there's only one path
94+
let delim = maybe_string_from_raw_ptr(delim_char).unwrap_or_else(|| "".to_owned());
95+
96+
// spec says if this is empty or null, show sendmail dialog without attachments
97+
let packed_paths = maybe_string_from_raw_ptr(file_paths).unwrap_or_else(|| "".to_owned());
98+
// spec says if this is empty or null, ignore
99+
let packed_names = maybe_string_from_raw_ptr(file_names).unwrap_or_else(|| "".to_owned());
100+
101+
let paths = unpack_strings(packed_paths, &delim);
102+
let names = unpack_strings(packed_names, &delim);
103+
104+
let msg = Message::from_paths(paths, names);
105+
106+
commands::log_to_file("mapisenddocument", "parsed documents, sending...");
107+
if let Err(e) = send_mail(msg) {
108+
commands::log_to_file("mapisenddocument", &format!("could not send mail: {:?}", e));
109+
MapiStatusCode::Failure
110+
} else {
111+
commands::log_to_file("mapisenddocument", "sent message!");
112+
MapiStatusCode::Success
113+
}
93114
}
94115

95116
/// https://docs.microsoft.com/en-us/windows/win32/api/mapi/nc-mapi-mapifindnext

src/structs/file_descriptor.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ impl TryFrom<&RawMapiFileDesc> for FileDescriptor {
113113
}
114114

115115
impl FileDescriptor {
116-
#[cfg(test)]
117116
pub fn new(file_path: &str, file_name: Option<&str>) -> Self {
118117
Self {
119118
flags: MapiFileFlags::empty(),

src/structs/message.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,34 @@ impl Message {
204204
files: attach,
205205
}
206206
}
207+
208+
pub fn from_paths(paths: Vec<String>, names: Vec<String>) -> Self {
209+
// if we got file names, but not the the same amount as paths, we
210+
// will use the file paths as-is
211+
let names: Vec<Option<&str>> = if names.is_empty() || names.len() != paths.len() {
212+
vec![None; paths.len()]
213+
} else {
214+
names.iter().map(AsRef::as_ref).map(Some).collect()
215+
};
216+
217+
let files = paths
218+
.into_iter()
219+
.zip(names.into_iter())
220+
.map(|(p, n)| FileDescriptor::new(&p, n))
221+
.collect();
222+
223+
Self {
224+
subject: None,
225+
note_text: None,
226+
message_type: None,
227+
date_received: None,
228+
conversation_id: None,
229+
flags: MapiMessageFlags::empty(),
230+
originator: None,
231+
recips: vec![],
232+
files,
233+
}
234+
}
207235
}
208236

209237
#[cfg(test)]

src/structs/recipient_descriptor.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,23 @@ impl TryFrom<*const RawMapiRecipDesc> for RecipientDescriptor {
5757

5858
impl From<&RawMapiRecipDesc> for RecipientDescriptor {
5959
fn from(raw: &RawMapiRecipDesc) -> Self {
60+
// some applications (Sage50) prefix the mail addresses with SMTP: which is
61+
// technically not valid, but we're going to make a best effort to allow this.
62+
// ":" is only allowed in quoted local parts so we're not going to destroy
63+
// valid mail addresses with this.
64+
let address = conversion::maybe_string_from_raw_ptr(raw.address).map(|a| {
65+
if a.starts_with("SMTP:") {
66+
a.strip_prefix("SMTP:").unwrap().to_owned()
67+
} else {
68+
a
69+
}
70+
});
71+
6072
RecipientDescriptor {
6173
recip_class: raw.recip_class,
6274
name: conversion::maybe_string_from_raw_ptr(raw.name)
6375
.unwrap_or_else(|| "MISSING_RECIP_NAME".to_owned()),
64-
address: conversion::maybe_string_from_raw_ptr(raw.address),
76+
address,
6577
entry_id: conversion::copy_c_array_to_vec(raw.entry_id, raw.eid_size as usize),
6678
}
6779
}
@@ -78,3 +90,33 @@ impl RecipientDescriptor {
7890
}
7991
}
8092
}
93+
94+
#[cfg(test)]
95+
mod test {
96+
use std::ffi::CStr;
97+
98+
use crate::structs::{RawMapiRecipDesc, RecipientDescriptor};
99+
100+
#[test]
101+
fn smtp_prefix_is_stripped() {
102+
let raw = |a: &str| RawMapiRecipDesc {
103+
reserved: 0,
104+
recip_class: 0,
105+
name: std::ptr::null(),
106+
address: CStr::from_bytes_with_nul(a.as_bytes()).unwrap().as_ptr(),
107+
eid_size: 0,
108+
entry_id: std::ptr::null(),
109+
};
110+
111+
let address1 = raw(&"SMTP:a@b.c\0");
112+
let address2 = raw(&"\"SMTP:a\"@b.c\0");
113+
assert_eq!(
114+
RecipientDescriptor::from(&address1).address,
115+
Some("a@b.c".to_owned())
116+
);
117+
assert_eq!(
118+
RecipientDescriptor::from(&address2).address,
119+
Some("\"SMTP:a\"@b.c".to_owned())
120+
);
121+
}
122+
}

0 commit comments

Comments
 (0)