Skip to content

Commit 13d2dba

Browse files
authored
Email sender admin action: direct sending + cc recipients. (#7965)
1 parent ceabe80 commit 13d2dba

File tree

2 files changed

+72
-61
lines changed

2 files changed

+72
-61
lines changed

app/lib/admin/actions/email_send.dart

Lines changed: 69 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:pub_dev/frontend/email_sender.dart';
6+
import 'package:pub_dev/shared/utils.dart';
7+
58
import '../../account/agent.dart';
69
import '../../account/backend.dart';
710
import '../../package/backend.dart';
811
import '../../publisher/backend.dart';
9-
import '../../service/email/backend.dart';
10-
import '../../shared/datastore.dart';
1112
import '../../shared/email.dart';
1213

1314
import '../models.dart';
@@ -33,6 +34,7 @@ The list of resolved emails will be deduplicated.
3334
options: {
3435
'to': 'A comma separated list of email addresses or subjects '
3536
'(the recipients of the messages).',
37+
'cc': '(optional) same as "to" with addresses that will be CC-d.',
3638
'from': 'The email address to impersonate (`support@pub.dev` by default).',
3739
'subject': 'The subject of the email message.',
3840
'body': 'The text content of the email body.',
@@ -61,63 +63,74 @@ The list of resolved emails will be deduplicated.
6163
to != null && to.isNotEmpty,
6264
'to must be given',
6365
);
66+
final cc = options['cc'];
67+
final inReplyTo = options['in-reply-to'];
68+
69+
final emailList = await _resolveEmails(to!);
70+
final ccEmailList = cc == null ? null : await _resolveEmails(cc);
71+
72+
try {
73+
await emailSender.sendMessage(EmailMessage(
74+
localMessageId: createUuid(),
75+
EmailAddress(from),
76+
emailList.map((v) => EmailAddress(v)).toList(),
77+
emailSubject!,
78+
emailBody!,
79+
ccRecipients:
80+
ccEmailList?.map((v) => EmailAddress(v)).toList() ?? const [],
81+
inReplyToLocalMessageId: inReplyTo,
82+
));
83+
return {
84+
'emails': emailList,
85+
if (ccEmailList != null) 'ccEmails': ccEmailList,
86+
'sent': true,
87+
};
88+
} catch (e, st) {
89+
return {
90+
'sent': false,
91+
'error': e.toString(),
92+
'stackTrace': st.toString(),
93+
};
94+
}
95+
},
96+
);
6497

65-
final emails = <String>{};
66-
for (final val in to!.split(',')) {
67-
final value = val.trim();
68-
if (isValidEmail(value)) {
69-
emails.add(value);
70-
continue;
71-
}
72-
final ms = ModerationSubject.tryParse(value);
73-
InvalidInputException.check(ms != null, 'Invalid subject: $value');
98+
Future<List<String>> _resolveEmails(String value) async {
99+
final emails = <String>{};
100+
for (final val in value.split(',')) {
101+
final value = val.trim();
102+
if (isValidEmail(value)) {
103+
emails.add(value);
104+
continue;
105+
}
106+
final ms = ModerationSubject.tryParse(value);
107+
InvalidInputException.check(ms != null, 'Invalid subject: $value');
74108

75-
switch (ms!.kind) {
76-
case ModerationSubjectKind.package:
77-
case ModerationSubjectKind.packageVersion:
78-
final pkg = await packageBackend.lookupPackage(ms.package!);
79-
if (pkg!.publisherId != null) {
80-
final list =
81-
await publisherBackend.getAdminMemberEmails(ms.publisherId!);
82-
emails.addAll(list.nonNulls);
83-
} else {
84-
final list = await accountBackend
85-
.lookupUsersById(pkg.uploaders ?? const <String>[]);
86-
emails.addAll(list.map((e) => e?.email).nonNulls);
87-
}
88-
break;
89-
case ModerationSubjectKind.publisher:
109+
switch (ms!.kind) {
110+
case ModerationSubjectKind.package:
111+
case ModerationSubjectKind.packageVersion:
112+
final pkg = await packageBackend.lookupPackage(ms.package!);
113+
if (pkg!.publisherId != null) {
90114
final list =
91115
await publisherBackend.getAdminMemberEmails(ms.publisherId!);
92116
emails.addAll(list.nonNulls);
93-
break;
94-
case ModerationSubjectKind.user:
95-
emails.add(ms.email!);
96-
break;
97-
default:
98-
throw InvalidInputException('Unknown subject kind: ${ms.kind}');
99-
}
117+
} else {
118+
final list = await accountBackend
119+
.lookupUsersById(pkg.uploaders ?? const <String>[]);
120+
emails.addAll(list.map((e) => e?.email).nonNulls);
121+
}
122+
break;
123+
case ModerationSubjectKind.publisher:
124+
final list =
125+
await publisherBackend.getAdminMemberEmails(ms.publisherId!);
126+
emails.addAll(list.nonNulls);
127+
break;
128+
case ModerationSubjectKind.user:
129+
emails.add(ms.email!);
130+
break;
131+
default:
132+
throw InvalidInputException('Unknown subject kind: ${ms.kind}');
100133
}
101-
102-
final inReplyTo = options['in-reply-to'];
103-
104-
final emailList = emails.toList()..sort();
105-
final entity = emailBackend.prepareEntity(EmailMessage(
106-
EmailAddress(from),
107-
emailList.map((v) => EmailAddress(v)).toList(),
108-
emailSubject!,
109-
emailBody!,
110-
inReplyToLocalMessageId: inReplyTo,
111-
));
112-
await withRetryTransaction(dbService, (tx) async {
113-
tx.insert(entity);
114-
});
115-
final sent = await emailBackend.trySendOutgoingEmail(entity);
116-
117-
return {
118-
'uuid': entity.uuid,
119-
'emails': emailList,
120-
'sent': sent,
121-
};
122-
},
123-
);
134+
}
135+
return emails.toList()..sort();
136+
}

app/test/admin/email_send_test.dart

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ void main() {
2222
}),
2323
);
2424
expect(rs1.output, {
25-
'uuid': isNotEmpty,
2625
'emails': ['a@pub.dev', 'b@pub.dev'],
27-
'sent': 2,
26+
'sent': true,
2827
});
29-
expect(fakeEmailSender.sentMessages, hasLength(2));
28+
expect(fakeEmailSender.sentMessages, hasLength(1));
3029
final sent = fakeEmailSender.sentMessages
3130
.expand((e) => e.recipients)
3231
.map((e) => e.email)
@@ -47,9 +46,8 @@ void main() {
4746
}),
4847
);
4948
expect(rs1.output, {
50-
'uuid': isNotEmpty,
5149
'emails': ['admin@pub.dev'],
52-
'sent': 1,
50+
'sent': true,
5351
});
5452
expect(fakeEmailSender.sentMessages, hasLength(1));
5553
final email = fakeEmailSender.sentMessages.single;

0 commit comments

Comments
 (0)