luftpost 1.0.0

Watches luftdaten.info particulates sensors and sends E-Mails if measurements exceed thresholds
Documentation
use config::Smtp;
use handlebars::{Handlebars, RenderError, RenderContext, Helper};
use check::CheckedMeasurement;
use lettre::email::{Email, EmailBuilder};
use lettre::transport::EmailTransport;
use lettre::transport::smtp::{SecurityLevel, SmtpTransport, SmtpTransportBuilder};
use lettre::transport::smtp::SUBMISSION_PORT;
use lettre::transport::stub::StubEmailTransport;
use lettre::transport::file::FileEmailTransport;

#[allow(large_enum_variant)]
error_chain!{
    errors {
    }
    foreign_links {
        FileTransportError(::lettre::transport::file::error::Error);
        SmtpTransportError(::lettre::transport::smtp::error::Error);
        StubTransportError(::lettre::transport::stub::error::Error);
        EmailFormatError(::lettre::email::error::Error);
        TemplateError(::handlebars::TemplateError);
        RenderError(::handlebars::RenderError);
        IoError(::std::io::Error);
    }
}

pub enum Transport {
    File(Box<FileEmailTransport>),
    Smtp(Box<SmtpTransport>),
    Stub(Box<StubEmailTransport>)
}

pub struct Mailer<'a> {
    pub transport: Transport,
    pub to_addr: &'a str,
    pub from_addr: &'a str,
    pub subject: &'a str,
    pub text_template: &'a str,
    pub html_template: &'a str,
}

impl<'a> Mailer<'a> {
    pub fn create_mailer(smtp: &'a Smtp) -> Result<Mailer<'a>> {
        let mut builder = SmtpTransportBuilder::new(
            (&smtp.server[..], smtp.port.unwrap_or_else(|| SUBMISSION_PORT))).unwrap()
            .hello_name("my.hostname.tld")
            .security_level(SecurityLevel::Opportunistic)
            .smtp_utf8(true)
            .connection_reuse(true);
        if smtp.username.is_some() && smtp.password.is_some() && smtp.auth_mechanism.is_some() {
            builder = builder
                .credentials(smtp.username.as_ref().unwrap(), smtp.password.as_ref().unwrap())
                .authentication_mechanism(smtp.auth_mechanism.unwrap());
        }
        let transport = builder.build();

        let mailer = Mailer {
            transport: Transport::Smtp(Box::new(transport)),
            to_addr: &smtp.receiver,
            from_addr: &smtp.sender,
            subject: &smtp.subject,
            text_template: &smtp.text_template,
            html_template: &smtp.html_template,
        };

        Ok(mailer)
    }

    pub fn mail_measurement(&mut self, check_measurement: &CheckedMeasurement) -> Result<()> {
        let (subject, text, html) = create_body(check_measurement, self.subject,
                                                self.text_template, self.html_template)?;
        let email = EmailBuilder::new()
            .to(self.to_addr)
            .from(self.from_addr)
            .subject(&subject)
            .alternative(&html, &text)
            .build()?;
        self.send(email)
    }

    fn send(&mut self, email: Email) -> Result<()> {
        match self.transport {
            Transport::File(ref mut file) => file.send(email).map(|_| ()).map_err(|e| e.into()),
            Transport::Smtp(ref mut smtp) => smtp.send(email).map(|_| ()).map_err(|e| e.into()),
            Transport::Stub(ref mut stub) => stub.send(email).map(|_| ()).map_err(|e| e.into()),
        }
    }
}

fn create_body(check_measurement: &CheckedMeasurement, subject_template: &str, text_template: &str, html_template: &str) -> Result<(String, String, String)> {
    let mut handlebars = Handlebars::new();
    handlebars.register_helper("number", Box::new(handlebars_number_formatter));
    handlebars.register_helper("exceeds", Box::new(handlebars_number_comparision));
    handlebars.register_template_string("subject", subject_template)?;
    handlebars.register_template_string("text_template", text_template)?;
    handlebars.register_template_string("html_template", html_template)?;
    let subject = handlebars.render("subject", check_measurement)?;
    let text = handlebars.render("text_template", check_measurement)?;
    let html = handlebars.render("html_template", check_measurement)?;

    Ok((subject, text, html))
}

fn handlebars_number_formatter(h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> ::std::result::Result<(), RenderError> {
    let param = h.param(0).unwrap();
    let number = param.value().as_f64().unwrap();

    let f = format!("{:.2}", number);
    rc.writer.write_all(f.as_bytes())?;

    Ok(())
}

fn handlebars_number_comparision(h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> ::std::result::Result<(), RenderError> {
    let param1 = h.param(0).unwrap();
    let param2 = h.param(1).unwrap();
    let number1 = param1.value().as_f64().unwrap();
    let number2 = param2.value().as_f64().unwrap();

    let f = if number1 > number2 {
        ">"
    } else if number1 < number2 {
        "<"
    } else {
        "="
    };
    rc.writer.write_all(f.as_bytes())?;

    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;

    use measurement::{Measurement, Value};
    use lettre::transport::stub::StubEmailTransport;
    use sensor::Sensor;

    #[test]
    fn mail_measurement_okay() -> () {
        let sensor = Sensor {
            name: "A Sensor".to_string(),
            id: "123456789".to_string(),
            ui_uri: "http://localhost".to_string(),
            data_uri: "http://localhost".to_string(),
            threshold_pm10: Some(10.0),
            threshold_pm2: Some(2.0),
            notification_condition: None,
        };
        let mut data_values = Vec::new();
        data_values.push(Value::SDS_P1(7.87f32));
        data_values.push(Value::SDS_P2(3.17f32));
        data_values.push(Value::TEMPERATURE(18.90f32));
        data_values.push(Value::HUMIDITY(49.10f32));
        data_values.push(Value::SAMPLES(739514f32));
        data_values.push(Value::MIN_MICRO(192f32));
        data_values.push(Value::MAX_MICRO(27599f32));
        data_values.push(Value::SIGNAL(-73f32));
        let measurement = Measurement {
            sensor: sensor,
            software_version: "NRZ-2017-089".to_string(),
            data_values: data_values,
        };
        let check_measurement = CheckedMeasurement {
            measurement: measurement,
            has_violations: false,
            violations: Vec::new(),
        };
        let mut mailer = Mailer {
            transport: Transport::Stub(Box::new(StubEmailTransport)),
            to_addr: "test@example.com",
            from_addr: "sender@example.com",
            subject: "Sensor {{ measurement.sensor.name }} exceeded thresholds",
            text_template: "{{ sensor.name }}",
            html_template: "{{ sensor.name }}"
        };

        let res = mailer.mail_measurement(&check_measurement);

        assert!(res.is_ok());
    }
}