Compare commits

...

20 Commits

Author SHA1 Message Date
pabzm
cece414af0 Add some information about mailing list managers. 2020-04-17 18:38:34 +02:00
Hocuri
2beb428c95 Add draft/mailing_list_current_state.rst 2020-04-17 18:19:32 +02:00
Hocuri
7227975175 Add Self and mailing list id as contacts; unfortunately I had to remove the may_be_valid_addr test because a listId is not a vaild email address 2020-04-17 12:45:22 +02:00
Hocuri
aa5ca4f816 Do not trucate subject after '[' 2020-04-17 11:12:19 +02:00
Hocuri
24ba96f6b2 Make the extended (with display name) address look nicer 2020-04-17 10:18:10 +02:00
Hocuri
b0a92a8727 Add test, bugfixing 2020-04-17 10:18:09 +02:00
Hocuri
ec18dcab64 First try using the whole name as identifier in mailing lists 2020-04-17 10:18:09 +02:00
Hocuri
8c3e6276c4 Update mailing_list.rst 2020-04-17 10:18:09 +02:00
Hocuri
762aa1ed2e Update mailing_list.rst 2020-04-17 10:18:08 +02:00
Hocuri
adcaec8665 Create mailing_list.rst 2020-04-17 10:18:08 +02:00
Hocuri
e575be2593 For mailing lists, always add the subject 2020-04-17 10:18:08 +02:00
Hocuri
b1536b3893 Code style (I hope it got better) 2020-04-17 10:18:08 +02:00
Hocuri
4e174d0c2f Make clear that create_group_record is also used by create_mailinglist_record 2020-04-17 10:18:07 +02:00
Hocuri
51e1826958 Do not count mailinglist contacts as 'known'. 2020-04-17 10:18:07 +02:00
Hocuri
7b3e6d185e Modify test so that it fails if contacts are added from a mailing list 2020-04-17 10:18:07 +02:00
Hocuri
c5374565ec Do not show "0 members" subtitle for mailing lists 2020-04-17 10:18:06 +02:00
Hocuri
3287e175b4 Improve testing, remove debugging println! statements 2020-04-17 10:18:06 +02:00
Hocuri
af1930dc8d Improving mailing lists and fixing things:
Remove additional chat type MailingList, make them GroupLists with a special param, make can_send() return false, code style and deduplication
2020-04-17 10:18:06 +02:00
Hocuri
b581a97edc Create test and get it to pass (debugging). Note that there still are some println!s in the code now. 2020-04-17 10:18:05 +02:00
Hocuri
3eda35c088 First try implementing mailing lists 2020-04-17 10:18:02 +02:00
9 changed files with 381 additions and 33 deletions

19
draft/mailing_list.rst Normal file
View File

@@ -0,0 +1,19 @@
2. Mailing lists are now only detected by the ListId header, because how should DC sort mailing lists if the ListId header is unset and only the precedence header says it's a mailing list (?). We could just hide these emails (with "junk" or "list" precedence), as we did before, if there is a reason to do so.
I do not actually understand why such emails were hidden in the first place, though; if there is an automatic answer stating that someone is out of office or if an email could not be delivered (these are the usual reasons why such emails are sent), I would want to know about this as a user.
Björn: to the ListId-header: it is already an improvement to use only that and to keep ignoring mails with Precedence-header. historically, i ignored all these mails to reduce noise that is created from them (eg. contacts should not be added to the suggestion list...) and to be able to concentrate on other things first.
3. for `List-Id: foo <bla>`, bla now is the id and foo is the name. For GitHub and GitLab notifications this is bad because all notifications will go into one chat. Maybe we should instead take the "in-reply-to" header to find out what messages belong together. For normal mailing lists, of course, this will create a second chat if someone does not use the "Reply" button to write to that mailing list.
Björn: well, having all notifications in one chat is not that bad. i would just follow the guidelines for now, use the ID and not the Name.
4. Currently a mailing list is shown as an empty group (ChatType `Group`) ("0 members").
Maybe we should change the ChatType to `Single` because this way, the UI would fit better. Disadvantage: We can't show the different senders of the messages.
Or we introduce a `ChatType` `MailingList`. Very big disadvantage: We would have to adapt all UI project and it would be nice if we could keep the changes within the core.
5. Contacts from mailings lists stay "unknown" now and are not shown in the contacts suggestion list..

View File

@@ -0,0 +1,13 @@
1. Mailing lists with Precedence-header and without List-Id are shown as normal messages.
2. for `List-Id: foo <bla>`, bla now is the id and foo is the name. If foo is not present, bla is taken as the name as well. For GitHub and GitLab notifications all notifications will go into one chat. To distinguish, all subjects are shown in mailing lists.
3. Currently a mailing list is shown as a group with SELF and the List-Id. I could not find any stable way to get a mail address that could be called the "mailing list address". To get this to work, I disabled the may_be_valid_addr check. (to be discussed...)
4. Contacts from mailings lists stay "unknown" and are not shown in the contacts suggestion list.
5. In general, only the From: address is taken as the author. If the domain matches with the List-Id domain (like `deltachat.github.com` and `Hocuri <notifications@notification.github.com>`) then the display name is added to the email address -> `Hocuri - notifications@notification.github.com`. This is not really nice, but as the UIs distinguish bettween contacts only based on the address this was the only way I could find without changing the API.
TODO: Prevent the user from sending emails to such contacts (like Hocuri - notifications@notification.github.com)
6. You can't send to mailing lists.

View File

@@ -0,0 +1,71 @@
# Mailing list integration of Delta Chat
A collection of information to help with the integration of mailing lists into Delta Chat.
## Chat name
How should the chat be titled?
It could be taken from the email header `List-Id`:
### Mailman
* Schema: `${list-description} <${list-local-part}.${list-domain}>`
* $list-description could be many words, should be truncated. Could also be empty, though.
### Schleuder
* Schema: `<${list-local-part}.${list-domain}>`
* No name available.
* Could be absent: <https://0xacab.org/schleuder/schleuder/-/blob/master/lib/schleuder/mail/message.rb#L357-358>.
### Sympa
* Schema: `<${list-local-part}.${list-domain}>`
* No name available.
* Always present <https://github.com/sympa-community/sympa/blob/sympa-6.2/src/lib/Sympa/Spindle/TransformOutgoing.pm#L110-L118>, <https://github.com/sympa-community/sympa/blob/sympa-6.2/src/lib/Sympa/List.pm#L6832-L6833>.
## Sender name
What's the name of the person that actually sent the message?
Some MLM change the sender information to avoid problems with DMARC.
### Mailman
* Since v2.1.16 the `From` depends on the list's configuration and possibly on the sender-domain's DMARC-configuration — i.e. messages from the same list can arrive one of the Variants A-D!
* Documentation of config options:
* <https://wiki.list.org/DOC/Mailman%202.1%20List%20Administrators%20Manual#line-544>,
* <https://wiki.list.org/DOC/Mailman%202.1%20List%20Administrators%20Manual#line-163>,
* <https://wiki.list.org/DEV/DMARC>
* Variant A: `From` is unchanged.
* Variant B. `From` is mangled like this: `${sender-name} via ${list-name} <${list-addr-spec}>`. The original sender is put into `Reply-To`.
* Variant C. `From` is mangled like this: `${sender-name} <${encoded-sender-addr-spec}@${list-domain}>`. The original sender is put into `Reply-To`.
* Variant D: `From` is set to ${list-name-addr}, the originally sent message is included as mime-part.
* Variant E: `From` is set to ${list-name-addr}, original sender information is removed.
### Schleuder
* Visible only in mime-body (which is possibly encrypted), or not at all.
* The first `text/plain` mime-part may include the information, as taken from the original message: `From: ${sender-name-addr}`.
### Sympa
* Depends on the list's configuration.
* Documentation:
* <https://sympa-community.github.io/manual/customize/dmarc-protection.html>,
* <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html#dmarc-protection>.
* Variant A: `From` is unchanged.
* Variant B: `From` is mangled like Mailman Variant B, but the original sender information is put into `X-Original-From`.
## Autocrypt
### Mailman
* Not supported.
### Schleuder
* A list's key is included in sent messages (as of version 3.5.0) (optional, by default active): <https://0xacab.org/schleuder/schleuder/-/blob/master/lib/schleuder/mail/message.rb#L349-355>.
* Incoming keys are not yet looked at (that feature is planned: <https://0xacab.org/schleuder/schleuder/issues/435>).
### Sympa
* Not supported.

View File

@@ -574,9 +574,13 @@ impl Chat {
self.param.exists(Param::Devicetalk)
}
pub fn is_mailing_list(&self) -> bool {
self.param.exists(Param::MailingList)
}
/// Returns true if user can send messages to this chat.
pub fn can_send(&self) -> bool {
!self.id.is_special() && !self.is_device_talk()
!self.id.is_special() && !self.is_device_talk() && !self.is_mailing_list()
}
pub fn update_param(&mut self, context: &Context) -> Result<(), Error> {
@@ -625,6 +629,10 @@ impl Chat {
.unwrap_or_else(|| "Err".into());
}
if self.typ == Chattype::Group && self.is_mailing_list() {
return "".to_string();
}
if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup {
if self.id.is_deaddrop() {
return context.stock_str(StockMessage::DeadDrop).into();
@@ -1097,7 +1105,11 @@ pub fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result<ChatId, Erro
msg_id: MsgId::new(0),
});
}
Contact::scaleup_origin_by_id(context, msg.from_id, Origin::CreateChat);
// If the message is from a mailing list, the contacts are not counted as "known"
if !chat.is_mailing_list() {
Contact::scaleup_origin_by_id(context, msg.from_id, Origin::CreateChat);
}
Ok(chat.id)
}
@@ -2502,6 +2514,22 @@ pub(crate) fn get_chat_id_by_grpid(
)
}
pub(crate) fn get_chat_id_by_mailinglistid(
context: &Context,
listid: impl AsRef<str>,
) -> Result<(ChatId, Blocked), sql::Error> {
context.sql.query_row(
"SELECT id, blocked, type FROM chats WHERE grpid=?;",
params![listid.as_ref()],
|row| {
let chat_id = row.get::<_, ChatId>(0)?;
let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
Ok((chat_id, b))
},
)
}
/// Adds a message to device chat.
///
/// Optional `label` can be provided to ensure that message is added only once.

View File

@@ -343,20 +343,6 @@ impl Contact {
return Ok((DC_CONTACT_ID_SELF, sth_modified));
}
if !may_be_valid_addr(&addr) {
warn!(
context,
"Bad address \"{}\" for contact \"{}\".",
addr,
if !name.as_ref().is_empty() {
name.as_ref()
} else {
"<unset>"
},
);
bail!("Bad address supplied: {:?}", addr);
}
let mut update_addr = false;
let mut update_name = false;
let mut update_authname = false;

View File

@@ -2,6 +2,8 @@ use itertools::join;
use sha2::{Digest, Sha256};
use num_traits::FromPrimitive;
use regex::Regex;
use std::borrow::Cow;
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
@@ -71,6 +73,8 @@ pub fn dc_receive_imf(
let mut created_db_entries = Vec::new();
let mut create_event_to_send = Some(CreateEvent::MsgsChanged);
let list_id_header: Option<&String> = mime_parser.get(HeaderDef::ListId);
// helper method to handle early exit and memory cleanup
let cleanup = |context: &Context,
create_event_to_send: &Option<CreateEvent>,
@@ -98,7 +102,7 @@ pub fn dc_receive_imf(
// https://github.com/deltachat/deltachat-core/issues/150)
let (from_id, from_id_blocked, incoming_origin) =
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
from_field_to_contact_id(context, field_from)?
from_field_to_contact_id(context, field_from, list_id_header)?
} else {
(0, false, Origin::Unknown)
};
@@ -117,6 +121,7 @@ pub fn dc_receive_imf(
} else {
Origin::IncomingUnknownTo
},
list_id_header,
)?);
}
}
@@ -230,11 +235,13 @@ pub fn dc_receive_imf(
pub fn from_field_to_contact_id(
context: &Context,
field_from: &str,
list_id_header: Option<&String>,
) -> Result<(u32, bool, Origin)> {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
list_id_header,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
@@ -415,9 +422,34 @@ fn add_parts(
if chat_id.is_unset() {
// check if the message belongs to a mailing list
if mime_parser.is_mailinglist_message() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "Message belongs to a mailing list and is ignored.",);
if let Some(list_id_header) = mime_parser.get(HeaderDef::ListId) {
let create_blocked = if !test_normal_chat_id.is_unset()
&& test_normal_chat_id_blocked == Blocked::Not
{
Blocked::Not
} else {
Blocked::Deaddrop
};
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_mailinglist(
context,
if test_normal_chat_id.is_unset() {
allow_creation
} else {
true
},
create_blocked,
list_id_header,
);
*chat_id = new_chat_id;
chat_id_blocked = new_chat_id_blocked;
if !chat_id.is_unset()
&& chat_id_blocked != Blocked::Not
&& create_blocked == Blocked::Not
{
new_chat_id.unblock(context);
chat_id_blocked = Blocked::Not;
}
}
}
@@ -1096,6 +1128,66 @@ fn create_or_lookup_group(
Ok((chat_id, chat_id_blocked))
}
fn create_or_lookup_mailinglist(
context: &Context,
allow_creation: bool,
create_blocked: Blocked,
list_id_header: &str,
) -> (ChatId, Blocked) {
let re = Regex::new(r"^(.*.)<(.*.)>$").unwrap();
let (name, listid) = match re.captures(list_id_header) {
Some(cap) => (cap[1].trim().to_string(), cap[2].trim().to_string()),
None => (
list_id_header.trim().to_string(),
list_id_header.trim().to_string(),
),
};
chat::get_chat_id_by_mailinglistid(context, &listid).unwrap_or_else(|_e| {
if allow_creation {
// list does not exist but should be created
match create_mailinglist_record(context, &listid, &name, create_blocked) {
Ok(chat_id) => {
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF);
// Add the mailing list as "unknown" contact
match add_or_lookup_contact_by_addr(
context,
&Some(name),
&listid,
Origin::IncomingUnknownFrom,
None,
) {
Ok(list_id_contact) => {
chat::add_to_chat_contacts_table(context, chat_id, list_id_contact);
}
Err(e) => warn!(
context,
"Failed to lookup mailing list contact: {}",
e.to_string()
),
};
(chat_id, create_blocked)
}
Err(e) => {
warn!(
context,
"Failed to create mailinglist '{}' for grpid={}: {}",
&name,
&listid,
e.to_string()
);
(ChatId::new(0), create_blocked)
}
}
} else {
info!(context, "creating list forbidden by caller");
(ChatId::new(0), Blocked::Not)
}
})
}
/// try extract a grpid from a message-id list header value
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
let header = mime_parser.get(headerdef)?;
@@ -1213,6 +1305,7 @@ fn create_or_lookup_adhoc_group(
Ok((new_chat_id, create_blocked))
}
// Insert a group record into the database. Note that this function is also used by create_mailinglist_record() below.
fn create_group_record(
context: &Context,
grpid: impl AsRef<str>,
@@ -1240,7 +1333,7 @@ fn create_group_record(
{
warn!(
context,
"Failed to create group '{}' for grpid={}",
"Failed to create group or mailinglist '{}' for grpid={}",
grpname.as_ref(),
grpid.as_ref()
);
@@ -1250,7 +1343,7 @@ fn create_group_record(
let chat_id = ChatId::new(row_id);
info!(
context,
"Created group '{}' grpid={} as {}",
"Created group or mailinglist '{}' grpid={} as {}",
grpname.as_ref(),
grpid.as_ref(),
chat_id
@@ -1258,6 +1351,27 @@ fn create_group_record(
chat_id
}
fn create_mailinglist_record(
context: &Context,
listid: impl AsRef<str>,
name: impl AsRef<str>,
create_blocked: Blocked,
) -> Result<ChatId> {
let chat_id = create_group_record(
context,
&listid,
&name,
create_blocked,
VerifiedStatus::Unverified,
);
let mut chat = Chat::load_from_db(context, chat_id)?;
chat.param.set(Param::MailingList, "true");
chat.update_param(context)?;
Ok(chat_id)
}
fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
/* algorithm:
- sort normalized, lowercased, e-mail addresses alphabetically
@@ -1579,6 +1693,7 @@ fn dc_add_or_lookup_contacts_by_address_list(
context: &Context,
addr_list_raw: &str,
origin: Origin,
list_id_header: Option<&String>,
) -> Result<ContactIds> {
let addrs = match mailparse::addrparse(addr_list_raw) {
Ok(addrs) => addrs,
@@ -1596,6 +1711,7 @@ fn dc_add_or_lookup_contacts_by_address_list(
&info.display_name,
&info.addr,
origin,
list_id_header,
)?);
}
mailparse::MailAddr::Group(infos) => {
@@ -1605,6 +1721,7 @@ fn dc_add_or_lookup_contacts_by_address_list(
&info.display_name,
&info.addr,
origin,
list_id_header,
)?);
}
}
@@ -1620,6 +1737,7 @@ fn add_or_lookup_contact_by_addr(
display_name: &Option<String>,
addr: &str,
origin: Origin,
list_id_header: Option<&String>,
) -> Result<u32> {
if context.is_self_addr(addr)? {
return Ok(DC_CONTACT_ID_SELF);
@@ -1629,8 +1747,26 @@ fn add_or_lookup_contact_by_addr(
.map(normalize_name)
.unwrap_or_default();
let mut addr = Cow::from(addr);
if let Some(list_id) = list_id_header {
let list_id = list_id.trim().trim_end_matches('>');
let addr_email = EmailAddress::new(&addr)?;
let mut addr_domain_parts = addr_email.domain.split('.');
let mut list_id_parts = list_id.split('.');
if list_id_parts.next_back() == addr_domain_parts.next_back()
&& list_id_parts.next_back() == addr_domain_parts.next_back()
{
// list_id was something like "name <name.github.com" (after trimming the last '>')
// and addr was something like "notifications@github.com".
// addr is not the address of the actual sender but the one of the mailing list.
// Add the display name to the addr to make it distinguishable from other people
// who sent to the same mailing list.
*addr.to_mut() = format!("{} {}", display_name_normalized, addr);
}
}
let (row_id, _modified) =
Contact::add_or_lookup(context, display_name_normalized, addr, origin)?;
Contact::add_or_lookup(context, display_name_normalized, &addr, origin)?;
ensure!(row_id > 0, "could not add contact: {:?}", addr);
Ok(row_id)
@@ -1660,6 +1796,97 @@ mod tests {
use crate::message::Message;
use crate::test_utils::{dummy_context, TestContext};
static MAILINGLIST: &[u8] = b"From: Max Mustermann <notifications@github.com>\n\
To: deltachat/deltachat-core-rust <deltachat-core-rust@noreply.github.com>\n\
Subject: [deltachat/deltachat-core-rust] PR run failed\n\
Message-ID: <3333@example.org>\n\
List-ID: deltachat/deltachat-core-rust <deltachat-core-rust.deltachat.github.com>\n\
Precedence: list\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n";
static MAILINGLIST2: &[u8] = b"From: Github <notifications@github.com>\n\
To: deltachat/deltachat-core-rust <deltachat-core-rust@noreply.github.com>\n\
Subject: [deltachat/deltachat-core-rust] PR run failed\n\
Message-ID: <3334@example.org>\n\
List-ID: deltachat/deltachat-core-rust <deltachat-core-rust.deltachat.github.com>\n\
Precedence: list\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello back\n";
static MAILINGLIST3: &[u8] = b"From: Alice <alice@posteo.org>\n\
To: delta-dev@codespeak.net\n\
Subject: Re: [delta-dev] What's up?\n\
Message-ID: <38942@posteo.org>\n\
List-ID: \"discussions about and around https://delta.chat developments\" <delta.codespeak.net>\n\
Precedence: list\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
body\n";
#[test]
fn test_mailing_list() {
let t = configured_offline_context();
t.ctx.set_config(Config::ShowEmails, Some("2")).unwrap();
dc_receive_imf(&t.ctx, MAILINGLIST, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
assert!(chat.is_mailing_list());
assert_eq!(chat.can_send(), false);
assert_eq!(chat.name, "deltachat/deltachat-core-rust");
println!("{:?}", Contact::load_from_db(&t.ctx, 1));
println!("{:?}", chat::get_chat_contacts(&t.ctx, chat_id));
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 2);
dc_receive_imf(&t.ctx, MAILINGLIST2, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
let contacts = Contact::get_all(&t.ctx, 0, None as Option<String>).unwrap();
assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts"
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None);
let contact1 = Contact::load_from_db(
&t.ctx,
Message::load_from_db(&t.ctx, msgs[0]).unwrap().from_id,
);
assert_eq!(
contact1.unwrap().get_addr(),
"Max Mustermann notifications@github.com"
);
let contact2 = Contact::load_from_db(
&t.ctx,
Message::load_from_db(&t.ctx, msgs[1]).unwrap().from_id,
);
assert_eq!(
contact2.unwrap().get_addr(),
"Github notifications@github.com"
);
}
#[test]
fn test_mailing_list2() {
let t = configured_offline_context();
t.ctx.set_config(Config::ShowEmails, Some("2")).unwrap();
dc_receive_imf(&t.ctx, MAILINGLIST3, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None);
let contact1 = Contact::load_from_db(
&t.ctx,
Message::load_from_db(&t.ctx, msgs[0]).unwrap().from_id,
);
assert_eq!(contact1.unwrap().get_addr(), "alice@posteo.org");
}
#[test]
fn test_hex_hash() {
let data = "hello world";

View File

@@ -1378,7 +1378,11 @@ fn prefetch_should_download(
.get_header_value(HeaderDef::From_)
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(
context,
&from_field,
headers.get_header_value(HeaderDef::ListId).as_ref(),
)?;
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message

View File

@@ -282,18 +282,16 @@ impl MimeMessage {
prepend_subject = false
}
}
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.get(HeaderDef::ListId).is_some() {
prepend_subject = true;
}
if prepend_subject {
let subj = if let Some(n) = subject.find('[') {
&subject[0..n]
} else {
subject
}
.trim();
if !subj.is_empty() {
if !subject.is_empty() {
for part in self.parts.iter_mut() {
if part.typ == Viewtype::Text {
part.msg = format!("{} {}", subj, part.msg);
part.msg = format!("{} {}", subject, part.msg);
break;
}
}

View File

@@ -117,6 +117,8 @@ pub enum Param {
/// For MDN-sending job
MsgId = b'I',
MailingList = b't',
}
/// Possible values for `Param::ForcePlaintext`.