From 44227d7b866f4aa173c63ffc989f38b44774e40d Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sat, 14 Oct 2023 21:03:42 -0300 Subject: [PATCH] fix: Put Message-ID into hidden headers and take it from there on receiver (#4798) Put a copy of Message-ID into hidden headers and prefer it over the one in the IMF header section that servers mess up with. This also reverts "Set X-Microsoft-Original-Message-ID on outgoing emails for amazonaws (#3077)". --- src/chat.rs | 2 +- src/mimefactory.rs | 93 ++++++++++------------ src/receive_imf.rs | 85 ++++++++++++++------ src/receive_imf/tests.rs | 16 ++++ test-data/message/messed_up_message_id.eml | 93 ++++++++++++++++++++++ 5 files changed, 213 insertions(+), 76 deletions(-) create mode 100644 test-data/message/messed_up_message_id.eml diff --git a/src/chat.rs b/src/chat.rs index d81108e39..8db0ee601 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -5904,7 +5904,7 @@ mod tests { // Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com). let sent_msg = alice.pop_sent_msg().await; let msg = sent_msg.payload(); - assert_eq!(msg.match_indices("Message-ID: , } @@ -560,24 +561,9 @@ impl<'a> MimeFactory<'a> { Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr), }; let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid); - - // Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do. - // Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`, - // and when downloading messages we look for this header in order to correctly identify - // messages. - // Amazon's servers do not add such a header, so we just add it ourselves. - if let Some(server) = context.get_config(Config::ConfiguredSendServer).await? { - if server.ends_with(".amazonaws.com") { - headers.unprotected.push(Header::new( - "X-Microsoft-Original-Message-ID".into(), - rfc724_mid_headervalue.clone(), - )) - } - } - - headers - .unprotected - .push(Header::new("Message-ID".into(), rfc724_mid_headervalue)); + let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue); + headers.unprotected.push(rfc724_mid_header.clone()); + headers.hidden.push(rfc724_mid_header); // Reply headers as in . if !self.in_reply_to.is_empty() { @@ -783,19 +769,14 @@ impl<'a> MimeFactory<'a> { ) .header(("Subject".to_string(), "...".to_string())) } else { - let message = if headers.hidden.is_empty() { - message - } else { - // Store hidden headers in the inner unencrypted message. - let message = headers - .hidden - .into_iter() - .fold(message, |message, header| message.header(header)); - - PartBuilder::new() - .message_type(MimeMultipartType::Mixed) - .child(message.build()) - }; + // Store hidden headers in the inner unencrypted message. + let message = headers + .hidden + .into_iter() + .fold(message, |message, header| message.header(header)); + let message = PartBuilder::new() + .message_type(MimeMultipartType::Mixed) + .child(message.build()); // Store protected headers in the outer message. let message = headers @@ -803,9 +784,7 @@ impl<'a> MimeFactory<'a> { .iter() .fold(message, |message, header| message.header(header.clone())); - if self.should_skip_autocrypt() - || !context.get_config_bool(Config::SignUnencrypted).await? - { + if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? { let protected: HashSet
= HashSet::from_iter(headers.protected.into_iter()); for h in headers.unprotected.split_off(0) { if !protected.contains(&h) { @@ -2165,33 +2144,37 @@ mod tests { let body = payload.next().unwrap(); assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); assert_eq!(outer.match_indices("Subject:").count(), 1); assert_eq!(outer.match_indices("Autocrypt:").count(), 1); assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); assert_eq!(inner.match_indices("Subject:").count(), 0); assert_eq!(body.match_indices("this is the text!").count(), 1); // if another message is sent, that one must not contain the avatar - // and no artificial multipart/mixed nesting let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(2, "\r\n\r\n"); + let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); let body = payload.next().unwrap(); - assert_eq!(outer.match_indices("text/plain").count(), 1); + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Message-ID:").count(), 1); assert_eq!(outer.match_indices("Subject:").count(), 1); assert_eq!(outer.match_indices("Autocrypt:").count(), 1); - assert_eq!(outer.match_indices("multipart/mixed").count(), 0); assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Message-ID:").count(), 1); + assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(inner.match_indices("Subject:").count(), 0); + assert_eq!(body.match_indices("this is the text!").count(), 1); - assert_eq!(body.match_indices("text/plain").count(), 0); - assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(body.match_indices("Subject:").count(), 0); Ok(()) } @@ -2223,6 +2206,7 @@ mod tests { let part = payload.next().unwrap(); assert_eq!(part.match_indices("multipart/signed").count(), 1); assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Subject:").count(), 0); assert_eq!(part.match_indices("Autocrypt:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); @@ -2234,6 +2218,7 @@ mod tests { 1 ); assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 1); assert_eq!(part.match_indices("Autocrypt:").count(), 0); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); @@ -2241,6 +2226,7 @@ mod tests { let part = payload.next().unwrap(); assert_eq!(part.match_indices("text/plain").count(), 1); assert_eq!(part.match_indices("From:").count(), 0); + assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1); assert_eq!(part.match_indices("Subject:").count(), 0); @@ -2261,31 +2247,38 @@ mod tests { .is_some()); // if another message is sent, that one must not contain the avatar - // and no artificial multipart/mixed nesting let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n"); + let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); let part = payload.next().unwrap(); assert_eq!(part.match_indices("multipart/signed").count(), 1); assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 1); assert_eq!(part.match_indices("Subject:").count(), 0); assert_eq!(part.match_indices("Autocrypt:").count(), 1); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); let part = payload.next().unwrap(); - assert_eq!(part.match_indices("text/plain").count(), 1); + assert_eq!( + part.match_indices("multipart/mixed; protected-headers=\"v1\"") + .count(), + 1 + ); assert_eq!(part.match_indices("From:").count(), 1); + assert_eq!(part.match_indices("Message-ID:").count(), 0); assert_eq!(part.match_indices("Subject:").count(), 1); assert_eq!(part.match_indices("Autocrypt:").count(), 0); - assert_eq!(part.match_indices("multipart/mixed").count(), 0); assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + let part = payload.next().unwrap(); + assert_eq!(part.match_indices("text/plain").count(), 1); + assert_eq!(body.match_indices("From:").count(), 0); + assert_eq!(part.match_indices("Message-ID:").count(), 1); + assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(part.match_indices("Subject:").count(), 0); + let body = payload.next().unwrap(); assert_eq!(body.match_indices("this is the text!").count(), 1); - assert_eq!(body.match_indices("text/plain").count(), 0); - assert_eq!(body.match_indices("From:").count(), 0); - assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(body.match_indices("Subject:").count(), 0); bob.recv_msg(&sent_msg).await; let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 8ffd88ac0..88ef7c09a 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -184,28 +184,49 @@ pub(crate) async fn receive_imf_inner( } } - info!(context, "Receiving message {rfc724_mid:?}, seen={seen}..."); + let rfc724_mid_orig = &mime_parser + .get_rfc724_mid() + .unwrap_or(rfc724_mid.to_string()); + info!( + context, + "Receiving message {rfc724_mid_orig:?}, seen={seen}...", + ); // check, if the mail is already in our database. // make sure, this check is done eg. before securejoin-processing. - let (replace_msg_id, replace_chat_id) = - if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? { - let msg = Message::load_from_db(context, old_msg_id).await?; - if msg.download_state() != DownloadState::Done && is_partial_download.is_none() { - // the message was partially downloaded before and is fully downloaded now. - info!( - context, - "Message already partly in DB, replacing by full message." - ); - (Some(old_msg_id), Some(msg.chat_id)) - } else { - // the message was probably moved around. - info!(context, "Message already in DB, doing nothing."); - return Ok(None); - } + let (replace_msg_id, replace_chat_id); + if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? { + if is_partial_download.is_some() { + info!( + context, + "Got a partial download and message is already in DB." + ); + return Ok(None); + } + let msg = Message::load_from_db(context, old_msg_id).await?; + replace_msg_id = Some(old_msg_id); + replace_chat_id = if msg.download_state() != DownloadState::Done { + // the message was partially downloaded before and is fully downloaded now. + info!( + context, + "Message already partly in DB, replacing by full message." + ); + Some(msg.chat_id) } else { - (None, None) + None }; + } else { + replace_msg_id = if rfc724_mid_orig != rfc724_mid { + message::rfc724_mid_exists(context, rfc724_mid_orig).await? + } else { + None + }; + replace_chat_id = None; + } + if replace_msg_id.is_some() && replace_chat_id.is_none() { + info!(context, "Message is already downloaded."); + return Ok(None); + }; let prevent_rename = mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some(); @@ -301,7 +322,7 @@ pub(crate) async fn receive_imf_inner( imf_raw, incoming, &to_ids, - rfc724_mid, + rfc724_mid_orig, from_id, seen || replace_msg_id.is_some(), is_partial_download, @@ -421,20 +442,30 @@ pub(crate) async fn receive_imf_inner( let delete_server_after = context.get_config_delete_server_after().await?; if !received_msg.msg_ids.is_empty() { - if received_msg.needs_delete_job + let target = if received_msg.needs_delete_job || (delete_server_after == Some(0) && is_partial_download.is_none()) { - let target = context.get_delete_msgs_target().await?; + Some(context.get_delete_msgs_target().await?) + } else { + None + }; + if target.is_some() || rfc724_mid_orig != rfc724_mid { + let target_subst = match &target { + Some(target) => format!("target='{target}',"), + None => "".to_string(), + }; context .sql .execute( - "UPDATE imap SET target=? WHERE rfc724_mid=?", - (target, rfc724_mid), + &format!("UPDATE imap SET {target_subst} rfc724_mid=?1 WHERE rfc724_mid=?2"), + (rfc724_mid_orig, rfc724_mid), ) .await?; - } else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() { + } + if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() + { // This is a Delta Chat MDN. Mark as read. - markseen_on_imap_table(context, rfc724_mid).await?; + markseen_on_imap_table(context, rfc724_mid_orig).await?; } } @@ -528,6 +559,10 @@ async fn add_parts( prevent_rename: bool, verified_encryption: VerifiedEncryption, ) -> Result { + let rfc724_mid_orig = &mime_parser + .get_rfc724_mid() + .unwrap_or(rfc724_mid.to_string()); + let mut chat_id = None; let mut chat_id_blocked = Blocked::Not; @@ -1308,7 +1343,7 @@ RETURNING id "#)?; let row_id: MsgId = stmt.query_row(params![ replace_msg_id, - rfc724_mid, + rfc724_mid_orig, if trash { DC_CHAT_ID_TRASH } else { chat_id }, if trash { ContactId::UNDEFINED } else { from_id }, if trash { ContactId::UNDEFINED } else { to_id }, diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 2c03c1ffb..255410b50 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -3223,6 +3223,22 @@ async fn test_thunderbird_unsigned_with_unencrypted_subject() -> Result<()> { Ok(()) } +/// Tests that DC takes the correct Message-ID from the encrypted message part, not the unencrypted +/// one messed up by the server. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_messed_up_message_id() -> Result<()> { + let t = TestContext::new_bob().await; + + let raw = include_bytes!("../../test-data/message/messed_up_message_id.eml"); + receive_imf(&t, raw, false).await?; + assert_eq!( + t.get_last_msg().await.rfc724_mid, + "0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mua_user_adds_member() -> Result<()> { let t = TestContext::new_alice().await; diff --git a/test-data/message/messed_up_message_id.eml b/test-data/message/messed_up_message_id.eml new file mode 100644 index 000000000..c9564c7aa --- /dev/null +++ b/test-data/message/messed_up_message_id.eml @@ -0,0 +1,93 @@ +From - Thu, 24 Nov 2022 19:06:16 GMT +X-Mozilla-Status: 0001 +X-Mozilla-Status2: 00800000 +Message-ID: <0bb9ffe1-2596-d997-95b4-1fef8cc4808f@example.org> +Date: Thu, 24 Nov 2022 20:05:57 +0100 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.4.2 +From: Alice +To: bob@example.net +Content-Language: en-US +Autocrypt: addr=alice@example.org; keydata= + xjMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5DN + GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp + 7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4M + CyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDr + RuI8A/8tEEXAA844BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp + 01JrRe6Xqy22HQMBCAfCeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsM + AAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIy + VfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg== +Subject: ... +Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + boundary="------------EOdOT2kJUL5hgCilmIhYyVZg" + +This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156) +--------------EOdOT2kJUL5hgCilmIhYyVZg +Content-Type: application/pgp-encrypted +Content-Description: PGP/MIME version identification + +Version: 1 + +--------------EOdOT2kJUL5hgCilmIhYyVZg +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Description: OpenPGP encrypted message +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- + +wV4D5tq63hTeebASAQdA1dVUsUjGZCOIfCnYtVdmOvKs/BNovI3sG8w1IH4ymTMwAZzgwVbGS5KL ++e1VTD5mUTeVSEYe1cd3VozH4KbNJa1tBlcO0nzGwCPpsTVDMoxIwcBMA+PY3JvEjuMiAQf/d2yj +t0+GyaptwX26bgSqo6vj21W8mcWS5vXOi8wjGwRbPaKKjS4kq1xDOz04eHrE8HUPD8otcXoI8CLz +etJpRbFs0XJP4Cozbsr72dgoWhozRg/iSpBndxWOddTl7Yqo8m/fyhU5uzKZ41m2T8mha6KkKWD8 +QecGdOgieYBucNBjHwWc71p9G6jTnzfy4S4GtGS2gwOSMxpwO7HxpKzsHI4POqFSQbxrl/YRwWSC +f5WqyYcerasIiR/fnOIw8lnvCeQ5rB90eGEDR70YFGt0t4rFBjfGrSPUiWYaTaC1Zvpd+t5sy7zy +FpsS2/aTkwP/UpGqmtFaD/brSouRf9hijNLI0QFTaVmSoI3BKzF8B4zwvtEbOLZjyDb+Va/fZJ3w +nYd2Q/5PPPL+pE4pWKN+jl0TZNzAaqBgvggXomgUqQ7QiksUzym+yuFKrJX0RF2awdrgjQIxjnda +Qp3UFphnFTyYUJpIU9iewjOfVxgPzv7PyuCHYwoP3kh7MJZ6bgbDmOkeFSnjEDJpdf1m9xC9LlBL +beC8scmPs6kx9GARBYSHvyPQ025gN3+XEHh4OrTxHZ91U3IlTfd2kACwOOAXEuhItSHmcNOV0K4M +nI2PH6gW8HgBkWlAPm40K4jUyo3nl1usDiI6ouvYqvW7YUc2hTtPTej1l2/mS57tTt+PFurKs555 +5R9DD/xg9Nx7OuQKy5bIdlXM20UmwuZTOhRJ5kpHFRzLxaHDbSzW+orhRW4llJSevBSAH3cLOjIQ +gh87j+MxG9j0TD2K2A0rcUcxdrnflw+mxcDVaL4payeqmOa+bJyhlftTqH+vqq5DhR68rX5VW+z7 +riqH3o8VbvO2y0XSpYHf1jowkfJj3vr8pynAUIv1dbylUSF5wtrHvzWOprw4bNrdtwQNRNy+JcVF +dUKeNmHaL6XOe4LUWpiI11beRyCpAG52khMCEAO3Q6+4e24cEipbu6suSOtv3OpYDZeHjwNrQIhi +rJg7i9TpMqwOeCvFWK+9UZ+P2n6h9g0/JO2+I82BFGUjVa5IvCTNOgv01GqxWY9ecdtaJjTc+dF2 +OAcRoKwvmtMJlxKEEgveui3BvPA4tuNdSrcoZBrQeo0ZHWVugXPvEZnwfZMcqwwPA+a/sUbZFg0P +Pr0AR0ZHpytnQE9OXE8wEUgT8H1yofQ+5QoZdgMpeAb8zGs+RuviLxcDkb9NtXUAiQ49ooWuFP3L +K9wMlaoWFTq7R+n5JVuSEYRCHC0l0bCV1/+awalT7XltXVCupI4lWzjYs52FZGGzuHG7S50Eufad +m4CQTPVgVaVn8WW2dmpMR8Gj8WbbZdyv21wMGOWjfgT0u3oiDnddGrFOoMNnZHch6rN3FRppoh7h +0U0fi8xxU1+EhUKq+fSIxZNr2iWN2if3Pipbxi9tyK9M41Y6aVF3HWjD58/OEql3aZjJZ1bqpXcE +qsPeFoXX78+7mTDvL75olMk2s/mg4mLqAAWQvTuoiOmj+SgMIFuTtFR+4r/TIFNdamz6AQ3RcmWG +ZcdRii+V27dtMA836vlAwxXRmJyE1LCL1kvUTq+J+AVsZi3xmBLFNlKPTlxswu7vSBrP1DlYOaBq +AgA0lKnkQdeXyDk/VdbTml7ywMW1g6HkFSqKGW/IIAObmBumBcIyHE6dWEHumRQomlJssIlEFSe+ +XEQ0rwedLetJXi5A0AXT1we1wvaKCEg0Pb0ZUxygwNPDrj6MmdodH7gDfyx0mW/7mEMCtIJb5MB+ +TRGPEa/vqdJb8uGtNXUy9UlwMhJ3tYoT7NXY4+IlNjbDH/yleMdwtWP2H2WH8oC+ysXPYXjlT8eU +poxRfJzPMVUn5SA3cvdGXDJWdX8U91j5sf9wuoYE5RBVrrJif3D3l0FpMrlWWoGw7wtZbMC2FaeT +QvdMS5c54IoXBtBTM+/AsTAw7WEE1QSmaQGHnh6xLL5Ns8olsWeKOMlVXdO9jSDbjOGBLr7mWukW +YzLXkH3TtJPQcbVN79af3YPhaHdMYITVKIwfg+vxZlLFHWLJQnkTl+9Qi7u2gKqkNeU7Zqs4E3CR +9K4dHrJMyAZLZ2HA1XQEj0/tMnbTpAzZhj02JRcFobLXK9SQfw7dzGZwMRky8cHcBHoK14P5RIEV +hr+38HSBM6wXtge5gL6DomAACvuORQO4X9x/CTjRt/J8uN3lKK5p+wi3ULeb319CEWiCiqmC1M+C +TADUhPUhUmTinSAVkTEn+BdbH/97dVaJnvd6HtLmdSlw4xqdWUfVL9Qd7+/5L6iwlOzGLKRv97c/ +gCRw+hzXyAom+5C18slSwanMuyPgIyrrFy/kp9Romk9SQr/c0CUF2am99t8G5qvVi/TiJGHyKEXD +aUYd4V7lqNlHMiiasvFHeq8blwmFr7rGEvbZzLNplc6sRUVlYhY2unRfyWsq9mqk3NDRW12Fa0J2 +YxQJlnXHQhNE8EyM/zsD9jCVNwsRZJ9/e5KS+ignmu6gKIR+ItDTwRfNI+NG/YmTgENUTyuO+vQC +CUKS3PCwpP+OEC966ARl7OCMdfn1hEyiAxsZnp1RmFngR6FM+mlGgfUoWNoHvnR1/YyQ4F4dadiA +QINwuSm5faw75F1EeL8Qi+LHKuqt05Pi/V9GJ6TzIkIsEbyyJ5sKHrp4QsU4C1p7ZhPjddz8De8k +6ZdwMIeXxi27WKtsFLcr8JKOBe0imIilKdMBOPS31pc1iJe4472WbWM0aBwdEYmnz9+xfOqnjHtO +0XTMjff7pzV6Y7t/u8J/zm3JS3ykote9HNRQvhZZNeVClVWd0fYFzat5ESnTojZTwHcc/BFTPnhz +VgLyw1KEIy2r3ZyGHu1b8GSYivzl33MOK/NVBQPZUIEfdcQ5vhkAvj+Yx340IYykRFEChwioprXD +LrIbTou7TNT5fTFA+beidHFsL+OE002/LMs6C3erSUW5C/LNjAQMS7cAV2yCyjX+/2GBmmDqnC4r +Ja2x5yik+fbOUPh3kk/md1YvrodlX/JkQeoWRrrVJsX2dr3BgivPJavaN0Jz1eHyxAYKNqlrfd1T +YWEDIisWerTxAVY/rEruZ6+OqLqOtZtn+4SOajOq8KFusglaMZqoYuM+LhPZck9PlZXwRqX08Vlv +8jX5V75BFWRhFd5/LYbnQHI6ZW80Wb2sBNngLL2QJT9yXGCDJb5qCdFwGd3i655pvRJXabeyCtDD +7I2PJcYRDd4stdq07BHyHJmye6vas8mG5QUygyWyUQv78za0m4gLMrRZBgoBDcVpWJUc+cPXzzfG +7PvLZu/Y0SaD5hqTp0LBB1PFxTpzdVeJ21gzVNQ6D4XGLTtdv4K4fOEYoeKEuzGoBaUDtIqz47gd +5rwfQ3ps2slkxfbtQcdKEACKvsCwzqHlgwsxD8QNOFzXYLiiiJBX22fIRoiJeSDMKSZyuFtpykCm +7bOpybPSHv3E7EIr8sIOr9MOe/R5HSthU2IgW1L5Ynr2t9HUnCA8CenkzIQjg0h5sruxcGWCYLx7 +q0f1AQs4Z7SebVbq1SCWVJNX/vc1bVjnjYfri7RX5WMmjJkuSnuIoP6a42cqJcAg7m0STB0elFAy +oO4vW9/JEmFUqLyQmWnoLJHX3IKtWa9CPvE= +=OA6b +-----END PGP MESSAGE----- + +--------------EOdOT2kJUL5hgCilmIhYyVZg--