Skip to content
This repository was archived by the owner on Oct 6, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 71 additions & 183 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,69 +27,15 @@ use rusoto_core::Region;
use rusoto_ses::{
Body, Content, Destination, Message, SendEmailRequest, Ses, SesClient,
};
use serde::{Deserialize, Serialize};
// use serde_json::Value;
use serde::Serialize;
use serde_json::Value;
use simple_logger::SimpleLogger;
use std::collections::HashMap;
use std::fmt::Debug;
use std::{fmt, process};
use tracing::log::LevelFilter;
use tracing::{error, info, warn};

// LambdaRequest: Represents the incoming Request from AWS Lambda
// This is deserialized into a struct payload
//
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct LambdaRequest {
/** lambda request body */
records: Vec<LambdaRequestRecord>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct LambdaRequestRecord {
/** event source */
event_source: String,

/** event version */
event_version: String,

/** event subscription arn*/
event_subscription_arn: String,

/** SNS Message body */
sns: SNSMessageBody,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct SNSMessageBody {
r#type: String,

message_id: String,

topic_arn: String,

subject: String,

/** SES Message request */
message: SesMessageRequest,

timestamp: String,

signature_version: u32,

signature: String,

signing_cert_url: String,

unsubscribe_url: String,

#[serde(default, skip_serializing_if = "HashMap::is_empty")]
message_attributes: HashMap<String, String>,
}

/// LambdaResponse: The Outgoing response being passed by the Lambda
#[derive(Debug, Default, Clone, Serialize)]
#[serde(default, rename_all = "camelCase")]
Expand Down Expand Up @@ -135,109 +81,10 @@ impl fmt::Display for LambdaResponse {
}
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct MessageHeader {
name: String,

value: String,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct ReceiptStatus {
status: String,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct LambdaInfo {
r#type: String,

topic_arn: String,

function_arn: String,

invocation_type: String,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct ReceiptInfo {
timestamp: String,

processing_time_millis: u32,

recipients: Vec<String>,

spam_verdict: ReceiptStatus,

virus_verdict: ReceiptStatus,

spf_verdict: ReceiptStatus,
dkim_verdict: ReceiptStatus,
dmarc_verdict: ReceiptStatus,
action: LambdaInfo,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct CommonHeaderReq {
return_path: String,

from: Vec<String>,

date: String,

to: Vec<String>,

cc: Vec<String>,

bcc: Vec<String>,

message_id: String,

subject: String,
}

#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct MailInfoReq {
timestamp: String,

source: String,

message_id: String,

destination: Vec<String>,

headers_truncated: bool,

headers: Vec<MessageHeader>,
common_headers: CommonHeaderReq,
}

/// SesMessageRequest: SES Message
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SesMessageRequest {
/** notification type */
notification_type: String,

/** receipt metadata **/
receipt: ReceiptInfo,

/** Email content */
content: String,

/** Email metadata */
mail: MailInfoReq,
}

/// PrivatEmail_Handler: processes incoming messages from SNS
/// and forwards to the appropriate recipient email
pub(crate) async fn privatemail_handler(
event: LambdaRequest,
event: Value,
ctx: Context,
) -> Result<LambdaResponse, Error> {
// Enable Cloudwatch error logging at runtime
Expand All @@ -251,53 +98,94 @@ pub(crate) async fn privatemail_handler(
let email_config = config::PrivatEmailConfig::new_from_env();

// Fetch request payload
let sns_payload: &LambdaRequestRecord = event.records.first().unwrap();
let sns_payload = event["Records"][0]["Sns"].as_object().unwrap();
info!("Raw Email Info: {:?}", sns_payload);

let email_info = &sns_payload.sns.message;
info!("Email Message: {:?}", email_info);
let new_email_info: &SesMessageRequest = email_info;
info!("Email NotificationType: {:#?}", new_email_info);

// skip spam messages
if new_email_info.receipt.spam_verdict.status == "FAIL"
|| new_email_info.receipt.virus_verdict.status == "FAIL"
if sns_payload["Message"]["receipt"]["spamVerdict"]["status"]
.as_str()
.unwrap()
== "FAIL"
|| sns_payload["Message"]["receipt"]["virusVerdict"]["status"]
.as_str()
.unwrap()
== "FAIL"
{
warn!("Message contains spam or virus, skipping!");
process::exit(200);
// Ok(LambdaResponse(200, "message skipped"))
}

// Rewrite Email From header to contain sender's name with forwarder's email address
let raw_from = sns_payload["Message"]["mail"]["commonHeaders"]
["returnPath"]
.as_str()
.unwrap()
.to_string();
let from: Vec<String> = vec![raw_from];

let to_emails: Option<Vec<String>> =
Some(vec![email_config.to_email.to_string()]);

info!(
"Email Subject: {:#?}",
sns_payload["Message"]["mail"]["commonHeaders"]["subject"]
.as_str()
.unwrap()
.to_string()
);
info!("From Email: {:#?}", from);
info!("To Email: {:#?}", to_emails);
info!(
"Email content: {:#?}",
sns_payload["Message"]["content"].as_str().unwrap().to_string()
);

let ses_email_message = SendEmailRequest {
configuration_set_name: None,
destination: Destination {
bcc_addresses: Some(new_email_info.mail.common_headers.bcc.clone()),
cc_addresses: Some(new_email_info.mail.common_headers.cc.clone()),
to_addresses: Some(vec![email_config.to_email.clone()]),
bcc_addresses: Some(vec!["".to_string()]),
cc_addresses: Some(vec!["".to_string()]),
to_addresses: to_emails,
},
message: Message {
body: Body {
html: Some(Content {
charset: Some(String::from("utf-8")),
data: new_email_info.content.to_string(),
data: sns_payload["Message"]["content"]
.as_str()
.unwrap()
.to_string(),
}),
text: Some(Content {
charset: Some(String::from("utf-8")),
data: new_email_info.content.to_string(),
data: sns_payload["Message"]["content"]
.as_str()
.unwrap()
.to_string(),
}),
},
subject: Content {
charset: Some(String::from("utf-8")),
data: new_email_info.mail.common_headers.subject.clone(),
data: sns_payload["Message"]["mail"]["commonHeaders"]
["subject"]
.as_str()
.unwrap()
.to_string(),
},
},
reply_to_addresses: Some(
new_email_info.mail.common_headers.from.clone(),
reply_to_addresses: Some(from),
return_path: Some(
sns_payload["Message"]["mail"]["source"]
.as_str()
.unwrap()
.to_string(),
),
return_path: Some(new_email_info.mail.source.to_string()),
return_path_arn: None,
source: new_email_info.mail.source.clone(),
source: sns_payload["Message"]["mail"]["source"]
.as_str()
.unwrap()
.to_string(),
source_arn: None,
tags: None,
};
Expand All @@ -318,29 +206,29 @@ pub(crate) async fn privatemail_handler(
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::{env, fs};

fn read_test_event() -> Result<LambdaRequest, Error> {
fn read_test_event() -> Value {
// Open the file in read-only mode with buffer.

let mut srcdir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
srcdir.push("tests/payload/testEvent.json");
println!("Cur Dir: {}", srcdir.display());
let file = File::open(srcdir)?;
let reader = BufReader::new(file);

// Read the JSON contents of the file as an instance of `User`.
let req = serde_json::from_reader(reader)?;
// Read the JSON contents of the file as an instance of `String`.
let input_str = fs::read_to_string(srcdir.as_path()).unwrap();
info!("Input str: {}", input_str);

// Return the `LambdaRequest`.
Ok(req)
// Return the `Value`.
return serde_json::from_str(input_str.as_str()).unwrap();
}

#[tokio::test]
// #[ignore]
async fn handler_handles() {
env::set_var("TO_EMAIL", "[email protected]");
env::set_var("FROM_EMAIL", "[email protected]");
let test_event = read_test_event();
assert_eq!(
privatemail_handler(test_event, Context::default())
Expand Down
8 changes: 4 additions & 4 deletions src/lib/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ impl PrivatEmailConfig {
/// As long as you have the `from_email` and `to_email` environment setup; this should work
pub fn new_from_env() -> Self {
PrivatEmailConfig {
from_email: env::var("FROM_EMAIL").unwrap(),
to_email: env::var("TO_EMAIL").unwrap(),
from_email: env::var("FROM_EMAIL").unwrap_or_default(),
to_email: env::var("TO_EMAIL").unwrap_or_default(),
subject_prefix: Some(String::from("PrivateMail: ")), // not currently used
email_bucket: None,
email_key_prefix: None,
Expand Down Expand Up @@ -117,8 +117,8 @@ mod tests {

#[test]
fn test_new_from_env_privatemail_config() {
env::set_var("from_email", "test_from");
env::set_var("to_email", "test_to");
env::set_var("FROM_EMAIL", "test_from");
env::set_var("TO_EMAIL", "test_to");

let new_config = PrivatEmailConfig::new_from_env();
assert_eq!(new_config.from_email.contains("test_from"), true);
Expand Down
4 changes: 2 additions & 2 deletions tests/payload/testEvent.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
},
{
"name": "Content-Type",
"value": "multipart/alternative; boundary="-- -- = _Part_16661649_1644988291 .1615939463851 ""
"value": "multipart/alternative; boundary='-- -- = _Part_16661649_1644988291 .1615939463851 '"
},
{
"name": "X-EmailType-Id",
Expand Down Expand Up @@ -152,7 +152,7 @@
}
},
"content": "Return-Path: <[email protected]>rnReceived: from rn2-txn-msbadger06103.apple.com (rn2-txn-msbadger06103.apple.com [17.111.110.98])rn by inbound-smtp.us-east-1.amazonaws.com with SMTP id 3hrf8kqv0k8oc7t86fadne11md91d90gdpsell01rn for [email protected];rn Wed, 17 Mar 2021 00:04:24 +0000 (UTC)rnX-SES-Spam-Verdict: PASSrnX-SES-Virus-Verdict: PASSrnReceived-SPF: pass (spfCheck: domain of email.apple.com designates 17.111.110.98 as permitted sender) client-ip=17.111.110.98; [email protected]; helo=rn2-txn-msbadger06103.apple.com;rnAuthentication-Results: amazonses.com;rn spf=pass (spfCheck: domain of email.apple.com designates 17.111.110.98 as permitted sender) client-ip=17.111.110.98; [email protected]; helo=rn2-txn-msbadger06103.apple.com;rn dkim=pass [email protected];rn dmarc=pass header.from=email.apple.com;rnX-SES-RECEIPT: AEFBQUFBQUFBQUFIc09Bb2pTTHBLZG1udTJTVk9OSmpyNG4xM0JNMmFPeVEwRlA1emk1TkdGa3FZOXhWNnBSb24zWmk5OGhOVWU1T2ViSmFoYjhGYUl0b09adi8yRTFUTXFTOThyUWZwNy8rSFZ1R2lDb0ZkM1A3N0I5a2FxU3RWZGlSaldZbXNiRjlEM0IrSEQ2VVhmQWVjQ3Z4UHBOMzIrVnpKbTEvcnpCMDRTMDYyKzBOQmw2OVpTZkYwY3Qrd2tGQkl6YW9USkxkV0pxNkhzTWJsWnVoejFtQmJXRm9mU1pZOUs5ZHd2a0dvZndUVTRvdzRmbURLM3FBUzF1L3Zna1NyUDNoandVV2tiZVp6Ymw4b09vK3UvcW16V3VsUmlPV01PV2FEeEJTNWxmNGhhazJ2L2c9PQ==rnX-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=dIPAKDWZKrlyZ4fALXLZjvhOHDU79YnlTq9c+I0bba3euy4ZFlxRt+acEMWdDlxNVbmWjvmZ60drbALSWfhqdIOWmbvd6XaMyqS6DIV9dEqo3bpFgghi0+OZAqs4Y7oTHFezbYw7bMikCiFpOezL5J68C29tiYgm1w6sZvrXfso=; c=relaxed/simple; s=ug7nbtf4gccmlpwj322ax3p6ow6yfsug; d=amazonses.com; t=1615939465; v=1; bh=lVSsD3MUG3sqOrlw8QZ2JcSqd/lMVBvvpboqic7Fllg=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;rnDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=email.apple.com;rnts=email0517; t=1615939463;rntbh=lVSsD3MUG3sqOrlw8QZ2JcSqd/lMVBvvpboqic7Fllg=;rnth=Date:From:To:Message-ID:Subject:Content-Type;rntb=LB+J4ywHV0ECEle/2saC11aFbomhd1IyMu9Ge8U/JXnoxei390LJY/Gm/QszMUoXZrnt r45Du9xMjJ9qMpAAMmxE6xrs5SkMFrmJXEkXVmyMC8bmgIbqOwjMt3XxuJUb7SKd+Irnt Pv/EKoIGPyBwbtb3GxY5uhq8wmL38Ch2kv/PMFNFlUf1aSDqvR+cTzvH1kVQXJeL+vrnt GDFlPLrP+2XzZvaUvY+oPpc1FrPNVwna9aWA8ZidOJw7UmNRucuVg67XNCXs5STc+Frnt c0uKHSvYnpC7JRhxufuhLqtkUrXij3rYcSBgor9GtYEkEQfqM7M+xkNB0k+2xI/uYirnt 4cykUakVC9r0w==rnDate: Wed, 17 Mar 2021 00:04:23 +0000 (GMT)rnFrom: Apple Store <[email protected]>rnTo: [email protected]: <[email protected]>rnSubject: Explore your Apple Watch with a Specialist online.rnMIME-Version: 1.0rnContent-Type: multipart/alternative; rntboundary="
}
},
"Timestamp": "2021-03-16T17:20:34.581Z",
"SignatureVersion": "1",
"Signature": "oWiAwndwB/YtyU3a1JKE7UHSXLsbdXGQIuCHMGoXy3s8JWXTz/OU3v/yezenhLz92k2nYDr8Py2hDxqZiQ+U/w/9UL2mECEbzcn3VKV3XurpX/aw85RPxv2JTqroY5jDsZkh1amx+tD9jD1KzH61UsBadfovYL16Pl6Ipovr4GbumjeDWdYG87nsTCvSCwlwVnfXO8lrdqh+AgC2FaYrrYFmi8ZHD+PZTggd4+0je8FAJmUwe8xCiUuSaiRDvEAIN+X3k50b+uJDTq9W8fvwV0c0DJEwlOyWU9hhWxp2LqM9MhdRLhSxJG62k+RVi7U75CDrLhNt5Agg7JRLyxjaqA==",
Expand Down