-
Notifications
You must be signed in to change notification settings - Fork 5.8k
/
Copy pathnewsletter.rs
419 lines (383 loc) · 15.4 KB
/
newsletter.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use anyhow::{anyhow, Result};
use aws_sdk_sesv2::{
operation::{
create_contact::CreateContactError, create_contact_list::CreateContactListError,
create_email_identity::CreateEmailIdentityError,
create_email_template::CreateEmailTemplateError,
},
types::{
Body, Contact, Content, Destination, EmailContent, EmailTemplateContent,
ListManagementOptions, Message, Template,
},
Client,
};
use std::io::{BufRead, Write};
use tracing::info;
const CONTACT_LIST_NAME: &str = "weekly-coupons-newsletter";
const TEMPLATE_NAME: &str = "weekly-coupons";
/// The SESWorkflow struct encapsulates the entire SES v2 Coupon Newsletter Workflow.
pub struct SESWorkflow<'a> {
client: Client,
stdin: &'a mut dyn BufRead,
stdout: &'a mut dyn Write,
verified_email: String,
}
impl<'a> SESWorkflow<'a> {
/// Creates a new instance of the SESWorkflow struct.
///
/// # Arguments
///
/// * `client` - The AWS SDK for Rust SES v2 client.
/// * `stdin` - A mutable reference to the standard input stream.
/// * `stdout` - A mutable reference to the standard output stream.
pub fn new(client: Client, stdin: &'a mut dyn BufRead, stdout: &'a mut dyn Write) -> Self {
Self {
client,
stdin,
stdout,
verified_email: "".into(),
}
}
/// Prepares the application by creating a verified email identity and a contact list.
pub async fn prepare_application(&mut self) -> Result<()> {
// Prompt the user for a verified email address
writeln!(self.stdout, "Enter the verified email address to use: ")?;
self.stdout.flush().unwrap();
let mut verified_email = String::new();
self.stdin.read_line(&mut verified_email).unwrap();
self.verified_email = verified_email.trim().to_string();
// snippet-start:[sesv2.rust.create-email-identity]
match self
.client
.create_email_identity()
.email_identity(self.verified_email.clone())
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Email identity created successfully.")?,
Err(e) => match e.into_service_error() {
CreateEmailIdentityError::AlreadyExistsException(_) => {
writeln!(
self.stdout,
"Email identity already exists, skipping creation."
)?;
}
e => return Err(anyhow!("Error creating email identity: {}", e)),
},
}
// snippet-end:[sesv2.rust.create-email-identity]
// Create the contact list
// snippet-start:[sesv2.rust.create-contact-list]
match self
.client
.create_contact_list()
.contact_list_name(CONTACT_LIST_NAME)
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Contact list created successfully.")?,
Err(e) => match e.into_service_error() {
CreateContactListError::AlreadyExistsException(_) => {
writeln!(
self.stdout,
"Contact list already exists, skipping creation."
)?;
}
e => return Err(anyhow!("Error creating contact list: {}", e)),
},
}
// snippet-end:[sesv2.rust.create-contact-list]
// snippet-start:[sesv2.rust.create-email-template]
let template_html =
std::fs::read_to_string("../resources/newsletter/coupon-newsletter.html")
.unwrap_or_else(|_| "Missing coupon-newsletter.html".to_string());
let template_text =
std::fs::read_to_string("../resources/newsletter/coupon-newsletter.txt")
.unwrap_or_else(|_| "Missing coupon-newsletter.txt".to_string());
// Create the email template
let template_content = EmailTemplateContent::builder()
.subject("Weekly Coupons Newsletter")
.html(template_html)
.text(template_text)
.build();
match self
.client
.create_email_template()
.template_name(TEMPLATE_NAME)
.template_content(template_content)
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Email template created successfully.")?,
Err(e) => match e.into_service_error() {
CreateEmailTemplateError::AlreadyExistsException(_) => {
writeln!(
self.stdout,
"Email template already exists, skipping creation."
)?;
}
e => return Err(anyhow!("Error creating email template: {}", e)),
},
}
// snippet-end:[sesv2.rust.create-email-template]
Ok(())
}
/// Gathers subscriber email addresses and sends welcome emails.
pub async fn gather_subscriber_emails(&mut self) -> Result<()> {
// Prompt the user for a base email address
writeln!(
self.stdout,
"Enter a base email address for subscribing (e.g., [email protected]): "
)?;
self.stdout.flush().unwrap();
let mut base_email = String::new();
self.stdin.read_line(&mut base_email).unwrap();
let base_email = base_email.trim().to_string();
// Create 3 variants of the email address as {user email}+ses-weekly-newsletter-{i}@{user domain}
let (user_email, user_domain) = base_email.split_once('@').unwrap();
let mut emails = Vec::with_capacity(3);
for i in 1..=3 {
let email = format!("{}+ses-weekly-newsletter-{}@{}", user_email, i, user_domain);
emails.push(email);
}
// Create a contact and send a welcome email for each email address
for email in emails {
// Create the contact
// snippet-start:[sesv2.rust.create-contact]
match self
.client
.create_contact()
.contact_list_name(CONTACT_LIST_NAME)
.email_address(email.clone())
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Contact created for {}", email)?,
Err(e) => match e.into_service_error() {
CreateContactError::AlreadyExistsException(_) => writeln!(
self.stdout,
"Contact already exists for {}, skipping creation.",
email
)?,
e => return Err(anyhow!("Error creating contact for {}: {}", email, e)),
},
}
// snippet-end:[sesv2.rust.create-contact]
// Send the welcome email
// snippet-start:[sesv2.rust.send-email.simple]
let welcome_html = std::fs::read_to_string("../resources/newsletter/welcome.html")
.unwrap_or_else(|_| "Missing welcome.html".to_string());
let welcome_txt = std::fs::read_to_string("../resources/newsletter/welcome.txt")
.unwrap_or_else(|_| "Missing welcome.txt".to_string());
let email_content = EmailContent::builder()
.simple(
Message::builder()
.subject(
Content::builder()
.data("Welcome to the Weekly Coupons Newsletter")
.build()?,
)
.body(
Body::builder()
.html(Content::builder().data(welcome_html).build()?)
.text(Content::builder().data(welcome_txt).build()?)
.build(),
)
.build(),
)
.build();
match self
.client
.send_email()
.from_email_address(self.verified_email.clone())
.destination(Destination::builder().to_addresses(email.clone()).build())
.content(email_content)
.send()
.await
{
Ok(output) => {
if let Some(message_id) = output.message_id {
writeln!(
self.stdout,
"Welcome email sent to {} with message ID {}",
email, message_id
)?;
} else {
writeln!(self.stdout, "Welcome email sent to {}", email)?;
}
}
Err(e) => return Err(anyhow!("Error sending welcome email to {}: {}", email, e)),
}
// snippet-end:[sesv2.rust.send-email.simple]
}
Ok(())
}
/// Sends the coupon newsletter to the subscribers.
pub async fn send_coupon_newsletter(&mut self) -> Result<()> {
// Retrieve the list of contacts
// snippet-start:[sesv2.rust.list-contacts]
let contacts: Vec<Contact> = match self
.client
.list_contacts()
.contact_list_name(CONTACT_LIST_NAME)
.send()
.await
{
Ok(list_contacts_output) => {
list_contacts_output.contacts.unwrap().into_iter().collect()
}
Err(e) => {
return Err(anyhow!(
"Error retrieving contact list {}: {}",
CONTACT_LIST_NAME,
e
))
}
};
// snippet-end:[sesv2.rust.list-contacts]
// Send the newsletter to each contact
for email in contacts {
let email = email.email_address.unwrap();
// snippet-start:[sesv2.rust.send-email.template]
let coupons = std::fs::read_to_string("../resources/newsletter/sample_coupons.json")
.unwrap_or_else(|_| r#"{"coupons":[]}"#.to_string());
let email_content = EmailContent::builder()
.template(
Template::builder()
.template_name(TEMPLATE_NAME)
.template_data(coupons)
.build(),
)
.build();
match self
.client
.send_email()
.from_email_address(self.verified_email.clone())
.destination(Destination::builder().to_addresses(email.clone()).build())
.content(email_content)
.list_management_options(
ListManagementOptions::builder()
.contact_list_name(CONTACT_LIST_NAME)
.build()?,
)
.send()
.await
{
Ok(output) => {
if let Some(message_id) = output.message_id {
writeln!(
self.stdout,
"Newsletter sent to {} with message ID {}",
email, message_id
)?;
} else {
writeln!(self.stdout, "Newsletter sent to {}", email)?;
}
}
Err(e) => return Err(anyhow!("Error sending newsletter to {}: {}", email, e)),
}
// snippet-end:[sesv2.rust.send-email.template]
}
Ok(())
}
/// Monitors the sending activity and provides insights.
pub async fn monitor(&mut self) -> Result<()> {
// Check if the user wants to review the monitoring dashboard
writeln!(
self.stdout,
"Do you want to review the monitoring dashboard? (y/n): "
)?;
self.stdout.flush().unwrap();
let mut response = String::new();
self.stdin.read_line(&mut response).unwrap();
if response.trim().eq_ignore_ascii_case("y") {
// Open the SES monitoring dashboard in the default browser
open::that("https://console.aws.amazon.com/ses/home#/account")?;
writeln!(
self.stdout,
"The SES monitoring dashboard has been opened in your default browser."
)?;
writeln!(
self.stdout,
"Review the sending activity, open and click rates, bounces, complaints, and more."
)?;
} else {
writeln!(self.stdout, "Skipping the monitoring dashboard review.")?;
}
writeln!(self.stdout, "Press any key to continue.")?;
self.stdout.flush().unwrap();
let mut response = String::new();
self.stdin.read_line(&mut response).unwrap();
Ok(())
}
/// Cleans up the resources created during the workflow.
pub async fn cleanup(&mut self) -> Result<()> {
info!("Cleaning up resources...");
// snippet-start:[sesv2.rust.delete-contact-list]
match self
.client
.delete_contact_list()
.contact_list_name(CONTACT_LIST_NAME)
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Contact list deleted successfully.")?,
Err(e) => return Err(anyhow!("Error deleting contact list: {e}")),
}
// snippet-end:[sesv2.rust.delete-contact-list]
// snippet-start:[sesv2.rust.delete-email-template]
match self
.client
.delete_email_template()
.template_name(TEMPLATE_NAME)
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Email template deleted successfully.")?,
Err(e) => {
return Err(anyhow!("Error deleting email template: {e}"));
}
}
// snippet-end:[sesv2.rust.delete-email-template]
// Delete the email identity
writeln!(
self.stdout,
"Do you want to delete the verified email identity? (y/n): "
)?;
self.stdout.flush().unwrap();
let mut response = String::new();
self.stdin.read_line(&mut response).unwrap();
if response.trim().eq_ignore_ascii_case("y") {
// snippet-start:[sesv2.rust.delete-email-identity]
match self
.client
.delete_email_identity()
.email_identity(self.verified_email.clone())
.send()
.await
{
Ok(_) => writeln!(self.stdout, "Email identity deleted successfully.")?,
Err(e) => {
return Err(anyhow!("Error deleting email identity: {}", e));
}
}
// snippet-end:[sesv2.rust.delete-email-identity]
} else {
writeln!(self.stdout, "Skipping deletion of email identity.")?;
}
info!("Cleanup completed.");
Ok(())
}
pub async fn run(&mut self) -> Result<()> {
self.prepare_application().await?;
self.gather_subscriber_emails().await?;
self.send_coupon_newsletter().await?;
self.monitor().await?;
Ok(())
}
pub fn set_verified_email(&mut self, verified_email: String) {
self.verified_email = verified_email;
}
}