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
use crate::channel::{ClosedChannel, Receiver};
use crate::interface::OutputConnector;
use crate::message::Message;
use crate::util::IntoOption;

use lettre::message::{header::ContentType, Attachment, Mailbox, MultiPart, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor};

use async_trait::async_trait;

/// Output connector that acts as a SMTP client
/// The service sends emails to the SMTP server.
/// The service name is added as first word of the subject following by space.
/// The arguments are added as a words to the subject separated by spaces.
#[derive(Default, Clone)]
pub struct SmtpClient {
    smtp_domain: String,
    email: String,
    password: String,
    sender_name: Option<String>,
}

impl SmtpClient {
    pub fn domain(mut self, value: impl Into<String>) -> Self {
        self.smtp_domain = value.into();
        self
    }

    pub fn email(mut self, value: impl Into<String>) -> Self {
        self.email = value.into();
        self
    }

    pub fn password(mut self, value: impl Into<String>) -> Self {
        self.password = value.into();
        self
    }

    /// Name alias for the email
    pub fn sender_name(mut self, value: impl IntoOption<String>) -> Self {
        self.sender_name = value.into_some();
        self
    }
}

#[async_trait]
impl OutputConnector for SmtpClient {
    async fn run(mut self: Box<Self>, mut receiver: Receiver) -> Result<(), ClosedChannel> {
        let address = self.email.parse::<Address>().unwrap();
        let user = address.user().to_string();
        let credentials = Credentials::new(user, self.password.into());

        let from = Mailbox::new(self.sender_name, address);
        let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(self.smtp_domain.as_ref())
            .unwrap()
            .credentials(credentials)
            .build();

        loop {
            let message = receiver.recv().await?;
            if let Some(email) = message_to_email(message, from.clone()) {
                if let Err(err) = mailer.send(email).await {
                    log::error!("Sending error: {}", err);
                }
            }
        }
    }
}

fn message_to_email(message: Message, from: Mailbox) -> Option<lettre::Message> {
    let to_address = message
        .user
        .parse::<Address>()
        .map_err(|err| log::error!("{}", err))
        .ok()?;

    let single_parts = message
        .attached_data
        .into_iter()
        .map(|(filename, filebody)| {
            Some(
                Attachment::new(filename).body(
                    filebody,
                    ContentType::parse("application/octet-stream")
                        .map_err(|err| log::error!("{}", err))
                        .ok()?,
                ),
            )
        })
        .collect::<Vec<_>>();

    let mut multipart = MultiPart::alternative().singlepart(SinglePart::plain(message.body));
    for single in single_parts {
        multipart = multipart.singlepart(single?);
    }

    let subject = message.args.join(" ");

    lettre::Message::builder()
        .from(from)
        .to(Mailbox::new(None, to_address))
        .subject(format!("{} {}", message.service_name, subject))
        .multipart(multipart)
        .map_err(|err| log::error!("{}", err))
        .ok()
}