Skip to content

Commit d6fc0ae

Browse files
authored
In-Reply-To id for outgoing emails (#7950)
1 parent 54801dc commit d6fc0ae

File tree

3 files changed

+37
-10
lines changed

3 files changed

+37
-10
lines changed

app/lib/fake/backend/fake_email_sender.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class FakeEmailSender implements EmailSender {
2828

2929
@override
3030
Future<void> sendMessage(EmailMessage message) async {
31-
message.verifyLocalMessageId();
31+
message.verifyLocalMessageIds();
3232
if (failNextMessageCount > 0) {
3333
failNextMessageCount--;
3434
throw SmtpClientCommunicationException('fake network problem');

app/lib/frontend/email_sender.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,17 @@ class _LoggingEmailSender implements EmailSender {
6767
final loggingEmailSender = _LoggingEmailSender();
6868

6969
Message _toMessage(EmailMessage input) {
70-
input.verifyLocalMessageId();
70+
input.verifyLocalMessageIds();
71+
final inReplyToMessageId = input.inReplyToLocalMessageId == null
72+
? null
73+
: '<${input.inReplyToLocalMessageId}@pub.dev>';
74+
final headers = {
75+
'Message-ID': '<${input.localMessageId}@pub.dev>',
76+
if (inReplyToMessageId != null) 'In-Reply-To': inReplyToMessageId,
77+
if (inReplyToMessageId != null) 'References': inReplyToMessageId,
78+
};
7179
return Message()
72-
..headers = {'Message-ID': '<${input.localMessageId}@pub.dev>'}
80+
..headers = headers
7381
..from = _toAddress(input.from)
7482
..recipients = input.recipients.map(_toAddress).toList()
7583
..ccRecipients = input.ccRecipients.map(_toAddress).toList()

app/lib/shared/email.dart

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ final _lenientEmailRegExp = RegExp(r'^\S+@\S+\.\S+$');
1717
final _strictEmailRegExp = RegExp(
1818
r'''[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$''');
1919

20+
/// Matches the local part of the email's message-id field, with relaxed rules:
21+
/// - checks that only valid characters are present
22+
/// - checks for minimum and maximum length
23+
final _messageIdLocalPartRegExp = RegExp(r'^[0-9a-zA-Z\-]{19,36}$');
24+
2025
final _invitesFrom = EmailAddress(
2126
_invitesAtPubDev,
2227
name: 'Dart package site invites',
@@ -100,9 +105,15 @@ class EmailMessage {
100105
///
101106
/// [localMessageId] is not required while in the message construction phase,
102107
/// but a must have when sending out the actual email. `EmailSender`
103-
/// implementation must call [verifyLocalMessageId] before accepting the email
108+
/// implementation must call [verifyLocalMessageIds] before accepting the email
104109
/// for delivery.
105110
final String? localMessageId;
111+
112+
/// The local part of a previous email sent by pub.dev, and to which the current
113+
/// email is a reply-to. This will become the local part of the `In-Reply-To` and
114+
/// the `References` SMTP headers.
115+
final String? inReplyToLocalMessageId;
116+
106117
final EmailAddress from;
107118
final List<EmailAddress> recipients;
108119
final List<EmailAddress> ccRecipients;
@@ -114,19 +125,27 @@ class EmailMessage {
114125
this.recipients,
115126
this.subject,
116127
String bodyText, {
128+
this.inReplyToLocalMessageId,
117129
this.localMessageId,
118130
this.ccRecipients = const <EmailAddress>[],
119131
}) : bodyText = reflowBodyText(bodyText);
120132

121-
/// Throws [ArgumentError] if the [localMessageId] field doesn't look like
122-
/// UUID or ULID.
133+
/// Throws [ArgumentError] if the [localMessageId] or the
134+
/// [inReplyToLocalMessageId] field doesn't look like an approved ID.
123135
///
124136
/// TODO: double-check that we follow https://www.jwz.org/doc/mid.html
125-
void verifyLocalMessageId() {
126-
final uuid = localMessageId;
127-
if (uuid == null || uuid.length < 25 || uuid.length > 36) {
128-
throw ArgumentError('Invalid uuid: `$uuid`');
137+
void verifyLocalMessageIds() {
138+
void verifyId(String? id) {
139+
if (id != null && !_messageIdLocalPartRegExp.hasMatch(id)) {
140+
throw ArgumentError('Invalid message-id local part: `$id`');
141+
}
142+
}
143+
144+
if (localMessageId == null) {
145+
throw ArgumentError('`localMessageId` must be initialized.');
129146
}
147+
verifyId(localMessageId);
148+
verifyId(inReplyToLocalMessageId);
130149
}
131150

132151
Map<String, Object?> toJson() {

0 commit comments

Comments
 (0)