aboutsummaryrefslogtreecommitdiff
path: root/src/mail.rs
blob: c77e2ad2e1e3e5403ad463bc7f6072a2fd3ee04c (plain)
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
use chrono::Utc;
use std::io::{Error, ErrorKind};
use std::process::{Output, Stdio};
use tokio::io::AsyncWriteExt;
use tokio::process::Command;

use crate::model::config::Config;

static FROM_NAME: &str = "Budget";
static FROM_ADDRESS: &str = "budget@guyonvarch.me";

#[derive(Clone)]
pub struct Recipient {
    pub name: String,
    pub address: String,
}

pub async fn send(
    config: &Config,
    recipients: Vec<Recipient>,
    subject: String,
    message: String,
) -> bool {
    let headers = format_headers(recipients.clone(), subject);

    log::info!(
        "Sending mail{}\n{}",
        if config.mock_mails { " (MOCK)" } else { "" },
        headers.clone()
    );

    if config.mock_mails {
        true
    } else {
        let recipient_addresses = recipients
            .clone()
            .into_iter()
            .map(|r| r.address)
            .collect::<Vec<String>>();

        // https://github.com/NixOS/nixpkgs/issues/90248
        let mut command = Command::new("/run/wrappers/bin/sendmail");
        command.kill_on_drop(true);
        command.arg("-f").arg(FROM_ADDRESS);
        command.arg("--").args(recipient_addresses);
        command
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());

        let message = format!("{}\n\n{}", headers, message);
        match spawn(command, &message.into_bytes()).await {
            Ok(output) => {
                if output.status.success() {
                    log::info!("Mail sent");
                    true
                } else {
                    match String::from_utf8(output.stderr) {
                        Ok(error) => log::error!("Error sending email: {}", error),
                        _ => log::error!("Error sending email"),
                    };
                    false
                }
            }
            Err(err) => {
                log::error!("Error spawning command: {:?}", err);
                false
            }
        }
    }
}

fn format_headers(recipients: Vec<Recipient>, subject: String) -> String {
    let recipients = recipients
        .into_iter()
        .map(|r| format_address(r.name, r.address))
        .collect::<Vec<String>>()
        .join(", ");

    format!(
        "Date: {}\nFrom: {}\nTo: {}\nSubject: {}",
        Utc::now().to_rfc2822(),
        format_address(FROM_NAME.to_string(), FROM_ADDRESS.to_string()),
        recipients,
        subject,
    )
}

fn format_address(name: String, address: String) -> String {
    format!("{} <{}>", name, address)
}

async fn spawn(mut command: Command, stdin: &[u8]) -> Result<Output, Error> {
    let mut process = command.spawn()?;
    process
        .stdin
        .as_mut()
        .ok_or(Error::new(ErrorKind::Other, "Getting mutable stdin"))?
        .write_all(stdin)
        .await?;
    process.wait_with_output().await
}