Merge branch 'malte/mime' into 'main'
Add MIME support See merge request husk-project/husk-milter!5
This commit is contained in:
@@ -58,7 +58,8 @@ RUN apt-get update -yqq && \
|
||||
openssl \
|
||||
sq \
|
||||
bind9-host \
|
||||
inotify-tools
|
||||
inotify-tools \
|
||||
mailutils
|
||||
|
||||
RUN adduser --shell /bin/bash --disabled-password --gecos "" hockeypuck
|
||||
|
||||
|
||||
@@ -18,17 +18,19 @@ else
|
||||
if [ "$BRANCH" != "" ] ; then
|
||||
git checkout $BRANCH
|
||||
fi
|
||||
cargo build
|
||||
ASSET_DIR=/tmp cargo build
|
||||
|
||||
HUSK_BIN=/opt/husk/husk-milter/target/debug/husk
|
||||
HUSK_DIR=/opt/husk/husk-milter/
|
||||
|
||||
cp /tmp/shell_completions/husk.bash /etc/bash_completion.d/
|
||||
fi
|
||||
|
||||
## start rsyslogd
|
||||
/usr/sbin/rsyslogd -n -iNONE &
|
||||
|
||||
## start postfix
|
||||
sudo postfix start
|
||||
sudo postfix start > /dev/null
|
||||
|
||||
## start and prepare postgres
|
||||
DOMAINS="example.com"
|
||||
|
||||
@@ -389,7 +389,7 @@ pub fn is_encrypted(body: &Bytes) -> bool {
|
||||
}
|
||||
|
||||
/// Encrypt a message for `recipients`.
|
||||
pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipient>)
|
||||
pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipient>)
|
||||
-> anyhow::Result<Bytes> {
|
||||
|
||||
let mut keys = Vec::new();
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ impl HuskConfig {
|
||||
Err(HuskError::ConfigFileOpenError(format!(
|
||||
"{} - {}",
|
||||
path,
|
||||
e.to_string())).into())
|
||||
e)).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-12
@@ -22,7 +22,7 @@ use crate::types::husk_context::HuskContext;
|
||||
use crate::config::{HuskConfig, HuskConfigContainer};
|
||||
use crate::mail;
|
||||
use crate::common::crypto;
|
||||
use crate::types::mail_context::ProtectionPossibility;
|
||||
use crate::types::mail_context::{MailContext, ProtectionPossibility};
|
||||
|
||||
pub struct Daemon { }
|
||||
|
||||
@@ -149,25 +149,30 @@ impl Daemon {
|
||||
log::debug!("DATA");
|
||||
|
||||
if let Some(ref mut _context) = cx.data {
|
||||
// XXX: check for early accepting
|
||||
// if there are no recipients with authenticated certificates
|
||||
// tell the MTA that this milter is done and the mail can be further
|
||||
// processed.
|
||||
Status::Continue
|
||||
} else {
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_header(_cx: &mut Context<HuskContext<'_>>, name: CString, value: CString) -> Status {
|
||||
log::debug!("HEADER: {name:?} = {value:?}");
|
||||
|
||||
Status::Continue
|
||||
async fn handle_header(cx: &mut Context<HuskContext<'_>>, key: CString, value: CString) -> Status {
|
||||
log::debug!("HEADER: {key:?} = {value:?}");
|
||||
if let Some(ref mut context) = cx.data {
|
||||
context.add_to_header(key, value);
|
||||
Status::Continue
|
||||
} else {
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_eoh(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
||||
log::debug!("EOH");
|
||||
|
||||
// XXX: check for early accepting
|
||||
// if there are no recipients with authenticated certificates
|
||||
// tell the MTA that this milter is done and the mail can be further
|
||||
// processed.
|
||||
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
@@ -198,7 +203,6 @@ impl Daemon {
|
||||
return Status::Accept;
|
||||
}
|
||||
|
||||
|
||||
// possible protection
|
||||
// none -> accept
|
||||
// partial -> accept (later: split)
|
||||
@@ -207,11 +211,35 @@ impl Daemon {
|
||||
let protection = ProtectionPossibility::from(&context.mail);
|
||||
|
||||
if protection == ProtectionPossibility::Full {
|
||||
|
||||
let inner_mail = context.mail.generate_inner_mail(body);
|
||||
|
||||
// if an error occures the body is not exchanged
|
||||
match crypto::encrypt(context, &body, &context.mail.recipients) {
|
||||
match crypto::encrypt(context, inner_mail.to_string().as_bytes(), &context.mail.recipients) {
|
||||
Ok(encrypted) => {
|
||||
if cx.actions.replace_body(&encrypted).await.is_err() {
|
||||
// replace the body of the mail with an encrypted,
|
||||
// mime encoded version.
|
||||
let outer_mail = MailContext::generate_outer_mail(encrypted);
|
||||
if cx.actions.replace_body(
|
||||
outer_mail
|
||||
.body_to_string()
|
||||
.as_bytes()).await.is_err() {
|
||||
log::error!("Cannot exchange body");
|
||||
return Status::Continue;
|
||||
}
|
||||
|
||||
// adjust headers
|
||||
// This is mostly Content-Type related.
|
||||
for line in outer_mail.get_header().iter_lines() {
|
||||
// XXX: handle errors
|
||||
if let Ok(key) = line.key_as_cstring() {
|
||||
if let Ok(value) = line.value_as_cstring() {
|
||||
let _ = cx.actions.change_header(
|
||||
key, 1,
|
||||
Some(value)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
//! CHeader struct
|
||||
//!
|
||||
//! This struct is for collecting header lines from the milter.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::fmt;
|
||||
|
||||
pub struct CHeader {
|
||||
lines: Vec<(CString, CString)>
|
||||
}
|
||||
|
||||
impl CHeader {
|
||||
pub fn new() -> Self {
|
||||
CHeader {
|
||||
lines: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(mut self, key: CString, value: CString) -> Self {
|
||||
self.lines.push((key, value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn append(&mut self, key: CString, value: CString) {
|
||||
self.lines.push((key, value));
|
||||
}
|
||||
|
||||
pub fn find(&self, key: &str) -> Option<(&str, &str)> {
|
||||
let lower_key = key.to_lowercase();
|
||||
for (line_key, line_value) in self.lines.iter() {
|
||||
if let Ok(line_compare) = line_key.to_str() {
|
||||
if line_compare.to_ascii_lowercase() == lower_key {
|
||||
if let Ok(line_value_as_str) = line_value.to_str() {
|
||||
return Some(
|
||||
(line_compare, line_value_as_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_value_or<'ch>(&'ch self, key: &str, default: &'ch str) -> &'ch str {
|
||||
if let Some((_, value)) = self.find(key) {
|
||||
value
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_lines(&self) -> std::slice::Iter<'_, (CString, CString)> {
|
||||
self.lines.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CHeader {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", [
|
||||
self.iter_lines()
|
||||
.filter_map(|(k, v)| {
|
||||
match (k.to_str(), v.to_str()) {
|
||||
(Ok(k), Ok(v)) => Some([k, v].join(": ")),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n"),
|
||||
"\r\n".to_string()]
|
||||
.join("")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::ffi::CString;
|
||||
use super::CHeader;
|
||||
|
||||
|
||||
#[test]
|
||||
pub fn basic_functionality() {
|
||||
let cheader = CHeader::new()
|
||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||
.add(CString::new("Content-Transfer-Encoding").unwrap(), CString::new("7bit").unwrap());
|
||||
|
||||
assert_eq!("Content-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n", cheader.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn find() {
|
||||
let cheader = CHeader::new()
|
||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||
.add(CString::new("Content-Transfer-Encoding").unwrap(), CString::new("7bit").unwrap());
|
||||
|
||||
let line = cheader.find("content-type");
|
||||
|
||||
assert!(line.is_some());
|
||||
|
||||
let (key, value) = line.unwrap();
|
||||
|
||||
assert_eq!("Content-Type", key);
|
||||
assert_eq!("text/plain", value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn find_none() {
|
||||
let cheader = CHeader::new()
|
||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||
.add(CString::new("Content-Transfer-Encoding").unwrap(), CString::new("7bit").unwrap());
|
||||
|
||||
let line = cheader.find("foo-bar");
|
||||
|
||||
assert!(line.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn find_with_default() {
|
||||
let cheader = CHeader::new()
|
||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||
.add(CString::new("Content-Transfer-Encoding").unwrap(), CString::new("7bit").unwrap());
|
||||
|
||||
let value = cheader.get_value_or("foo-bar", "baaz");
|
||||
|
||||
assert_eq!(value, "baaz");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
//! email.
|
||||
|
||||
use anyhow;
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use std::path::PathBuf;
|
||||
use std::ffi::CString;
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use sequoia_openpgp::Cert;
|
||||
use sequoia_cert_store::CertStore;
|
||||
use sequoia_directories::Home;
|
||||
@@ -55,11 +56,15 @@ impl<'hc> HuskContext<'hc> {
|
||||
}
|
||||
|
||||
/// If the `HuskContext` is reused for another email, `reset_mail`
|
||||
/// creates the neccessary context for it.
|
||||
/// creates the neccessary (new) context for it.
|
||||
pub fn reset_mail(&mut self) {
|
||||
self.mail = MailContext::new();
|
||||
}
|
||||
|
||||
pub fn add_to_header(&mut self, key: CString, value: CString) {
|
||||
self.mail.add_to_header(key, value);
|
||||
}
|
||||
|
||||
/// Store the sender of an email in the current `MailContext`.
|
||||
pub fn set_sender(&mut self, sender: String) {
|
||||
self.mail.set_sender(sender);
|
||||
|
||||
+129
-1
@@ -4,10 +4,16 @@
|
||||
//! to keep the bits and pieces together.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::ffi::CString;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use sequoia_cert_store::LazyCert;
|
||||
|
||||
use crate::types::recipient::Recipient;
|
||||
use crate::types::cheader::CHeader;
|
||||
use crate::types::mime::header_line::HeaderLine;
|
||||
use crate::types::mime::header::Header;
|
||||
use crate::types::mime::part::Part;
|
||||
use crate::types::mime::multi_part::MultiPart;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum ProtectionPossibility {
|
||||
@@ -23,7 +29,7 @@ pub enum ProtectionPossibility {
|
||||
pub struct MailContext<'mc> {
|
||||
pub sender: Option<String>,
|
||||
pub recipients: Vec<Recipient<'mc>>,
|
||||
// header
|
||||
pub header: CHeader,
|
||||
body: Vec<Bytes>,
|
||||
pub body_size: usize,
|
||||
}
|
||||
@@ -34,6 +40,17 @@ impl<'mc> MailContext<'mc> {
|
||||
MailContext {
|
||||
sender: None,
|
||||
recipients: Vec::new(),
|
||||
header: CHeader::new(),
|
||||
body: Vec::new(),
|
||||
body_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_header(header: CHeader) -> Self {
|
||||
MailContext {
|
||||
sender: None,
|
||||
recipients: Vec::new(),
|
||||
header,
|
||||
body: Vec::new(),
|
||||
body_size: 0,
|
||||
}
|
||||
@@ -43,6 +60,10 @@ impl<'mc> MailContext<'mc> {
|
||||
self.sender = Some(sender);
|
||||
}
|
||||
|
||||
pub fn add_to_header(&mut self, key: CString, value: CString) {
|
||||
self.header.append(key, value);
|
||||
}
|
||||
|
||||
pub fn add_recipient(&mut self, rcpt: String, certs: Vec<Arc<LazyCert<'mc>>>) {
|
||||
let mut recipient: Recipient = rcpt.into();
|
||||
recipient.certs = certs;
|
||||
@@ -63,6 +84,71 @@ impl<'mc> MailContext<'mc> {
|
||||
buffer.into()
|
||||
}
|
||||
|
||||
pub fn generate_inner_mail(&self, body: Bytes) -> MultiPart<'mc> {
|
||||
let header = &self.header;
|
||||
let inner = Part::with_body(body)
|
||||
.set_header(Header::new()
|
||||
.add(
|
||||
HeaderLine::new("Content-Type")
|
||||
.with(header.get_value_or("content-type", "text/plain")
|
||||
.to_string()))
|
||||
.add(
|
||||
HeaderLine::new("Content-Transfer-Encoding")
|
||||
.with(header.get_value_or("content-transfer-encoding", "7bit")
|
||||
.to_string()))
|
||||
);
|
||||
|
||||
let mut inner_mail = MultiPart::new()
|
||||
.add_part(inner);
|
||||
|
||||
inner_mail.add_header_line(
|
||||
HeaderLine::new("Content-Type")
|
||||
.with("multipart/mixed".to_string())
|
||||
.with(format!("boundary=\"{}\"", inner_mail.get_boundary()))
|
||||
.with("protected-headers=\"v1\"".to_string())
|
||||
);
|
||||
for key in ["From", "To", "Subject", "Message-Id"] {
|
||||
if let Some((_, value)) = header.find(key) {
|
||||
inner_mail.add_header_line(
|
||||
HeaderLine::new(key).with(value.to_string()));
|
||||
}
|
||||
}
|
||||
inner_mail
|
||||
}
|
||||
|
||||
pub fn generate_outer_mail<'b>(body: Bytes) -> MultiPart<'b> {
|
||||
let version = Part::with_body("Version: 1".into())
|
||||
.set_header(Header::new()
|
||||
.add(HeaderLine::new("Content-Type")
|
||||
.with("application/pgp-encrypted".to_string()))
|
||||
.add(HeaderLine::new("Content-Description")
|
||||
.with("PGP/MIME version identification".to_string()))
|
||||
);
|
||||
let cryptpart = Part::with_body(body)
|
||||
.set_header(Header::new()
|
||||
.add(HeaderLine::new("Content-Type")
|
||||
.with("application/octet-stream".to_string())
|
||||
.with("name=\"encrypted.asc\"".to_string()))
|
||||
.add(HeaderLine::new("Content-Description")
|
||||
.with("OpenPGP encrypted message".to_string()))
|
||||
.add(HeaderLine::new("Content-Disposition")
|
||||
.with("inline".to_string())
|
||||
.with("filename=\"encrypted.asc\"".to_string()))
|
||||
);
|
||||
|
||||
let mut outer = MultiPart::new()
|
||||
.add_part(version)
|
||||
.add_part(cryptpart);
|
||||
outer.add_header_line(
|
||||
HeaderLine::new("Content-Type")
|
||||
.with("multipart/encrypted".to_string())
|
||||
.with("protocol=\"application/pgp-encrypted\"".to_string())
|
||||
.with(format!("boundary=\"{}\"", outer.get_boundary())
|
||||
)
|
||||
);
|
||||
outer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Default Mailcontext
|
||||
@@ -94,3 +180,45 @@ impl From<&MailContext<'_>> for ProtectionPossibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::ffi::CString;
|
||||
use crate::types::cheader::CHeader;
|
||||
use crate::types::mail_context::MailContext;
|
||||
|
||||
#[test]
|
||||
fn test_inner_mail() {
|
||||
|
||||
let orig_header = CHeader::new()
|
||||
.add(CString::new("From").unwrap(), CString::new("alice@example.com").unwrap())
|
||||
.add(CString::new("To").unwrap(), CString::new("bob@example.com").unwrap())
|
||||
.add(CString::new("Subject").unwrap(), CString::new("The original Subject").unwrap())
|
||||
.add(CString::new("Content-Type").unwrap(), CString::new("test/plain").unwrap())
|
||||
.add(CString::new("Content-Transfer-Encoding").unwrap(), CString::new("7bit").unwrap())
|
||||
// should not be copied into the inner mail
|
||||
.add(CString::new("other-header").unwrap(), CString::new("should_not_appear").unwrap());
|
||||
|
||||
let orig_body = "This is the body".as_bytes().to_vec();
|
||||
|
||||
let context = MailContext::from_header(orig_header);
|
||||
let inner_mail = context.generate_inner_mail(orig_body.into());
|
||||
|
||||
let intended_result = [
|
||||
format!("Content-Type: multipart/mixed; boundary=\"{}\"; protected-headers=\"v1\"", inner_mail.get_boundary()),
|
||||
"From: alice@example.com".to_string(),
|
||||
"To: bob@example.com".to_string(),
|
||||
"Subject: The original Subject".to_string(),
|
||||
"".to_string(),
|
||||
format!("--{}", inner_mail.get_boundary()),
|
||||
"Content-Type: test/plain".to_string(),
|
||||
"Content-Transfer-Encoding: 7bit".to_string(),
|
||||
"".to_string(),
|
||||
"This is the body".to_string(),
|
||||
format!("--{}--", inner_mail.get_boundary()) ].join("\r\n");
|
||||
|
||||
assert_eq!(intended_result, inner_mail.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! a complete mime header
|
||||
|
||||
use std::fmt;
|
||||
use super::header_line::HeaderLine;
|
||||
|
||||
pub struct Header<'h> {
|
||||
lines: Vec<HeaderLine<'h>>
|
||||
}
|
||||
|
||||
impl<'h> Header<'h> {
|
||||
|
||||
pub fn new() -> Self {
|
||||
Header {
|
||||
lines: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a `HeaderLine`, this function returns the `Header` back to the
|
||||
/// caller.
|
||||
pub fn add(mut self, line: HeaderLine<'h>) -> Self {
|
||||
self.lines.push(line);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a `HeaderLine`.
|
||||
pub fn append(&mut self, line: HeaderLine<'h>) {
|
||||
self.lines.push(line);
|
||||
}
|
||||
|
||||
pub fn iter_lines(&self) -> std::slice::Iter<'_, HeaderLine<'_>> {
|
||||
self.lines.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Header<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", [
|
||||
self.lines.iter()
|
||||
.map(|line: &HeaderLine| line.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n"),
|
||||
"\r\n".into()].join("")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_functionality() {
|
||||
let header = Header::new()
|
||||
.add(HeaderLine::new("Content-Type").with("text/plain".to_string()))
|
||||
.add(HeaderLine::new("Content-Transfer-Encoding").with("7bit".to_string()));
|
||||
assert_eq!(header.to_string(), "Content-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//! header line
|
||||
//!
|
||||
//! A Line of a mime header. It consists of a `key` and a `value` which
|
||||
//! itself is a list of attributes.
|
||||
//!
|
||||
//! Example:
|
||||
//! "Content-Type: text/plain; charset=UTF-8"
|
||||
//! key: "Content-Type"
|
||||
//! attribute 1: "text/plain"
|
||||
//! attribute 2: "charset=UTF-8"
|
||||
|
||||
use std::fmt;
|
||||
use std::ffi::CString;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct HeaderLine<'h> {
|
||||
key: &'h str,
|
||||
value: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'h> HeaderLine<'h> {
|
||||
pub fn new(key: &'h str) -> Self {
|
||||
HeaderLine {
|
||||
key,
|
||||
value: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with(mut self, part: String) -> Self {
|
||||
self.value.push(part);
|
||||
self
|
||||
}
|
||||
|
||||
/// Combine the parts of `value` to a `String`. Add '; ' as a
|
||||
/// separator.
|
||||
pub fn value_to_string(&self) -> String {
|
||||
self.value.join("; ")
|
||||
}
|
||||
|
||||
pub fn value_size(&self) -> usize {
|
||||
self.value_to_string().len()
|
||||
}
|
||||
|
||||
pub fn key_as_cstring(&self) -> Result<CString> {
|
||||
CString::new(self.key)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn value_as_cstring(&self) -> Result<CString> {
|
||||
CString::new(self.value_to_string())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HeaderLine<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}",
|
||||
[self.key, self.value_to_string().as_str()].join(": ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_functionality() {
|
||||
let header_line = HeaderLine::new("Content-Type")
|
||||
.with("text/plain".to_string())
|
||||
.with("charset=UTF-8".to_string());
|
||||
assert_eq!(&header_line.to_string(), "Content-Type: text/plain; charset=UTF-8");
|
||||
assert_eq!(&header_line.value_to_string(), "text/plain; charset=UTF-8");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Multipart MIME
|
||||
//!
|
||||
//! This module contains code to generate a multipart mime message.
|
||||
//! (no parsing)
|
||||
|
||||
pub mod header_line;
|
||||
pub mod header;
|
||||
pub mod part;
|
||||
pub mod multi_part;
|
||||
@@ -0,0 +1,124 @@
|
||||
//! multipart mime
|
||||
|
||||
use std::fmt;
|
||||
use std::iter::repeat_with;
|
||||
|
||||
use super::{header::Header, header_line::HeaderLine, part::Part};
|
||||
|
||||
pub struct MultiPart<'m> {
|
||||
header: Header<'m>,
|
||||
parts: Vec<Part<'m>>,
|
||||
boundary: String
|
||||
}
|
||||
|
||||
impl<'m> MultiPart<'m> {
|
||||
|
||||
pub fn new() -> Self {
|
||||
MultiPart {
|
||||
header: Header::new(),
|
||||
parts: Vec::new(),
|
||||
boundary: MultiPart::new_boundary()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_boundary(boundary: String) -> Self {
|
||||
MultiPart {
|
||||
header: Header::new(),
|
||||
parts: Vec::new(),
|
||||
boundary: boundary
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_part(mut self, part: Part<'m>) -> Self {
|
||||
self.parts.push(part);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_header(mut self, header: Header<'m>) -> Self {
|
||||
self.header = header;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_header(&self) -> &Header<'_> {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub fn add_header_line(&mut self, line: HeaderLine<'m>) {
|
||||
self.header.append(line);
|
||||
}
|
||||
|
||||
pub fn get_boundary(&self) -> String {
|
||||
self.boundary.clone()
|
||||
}
|
||||
|
||||
pub fn new_boundary() -> String {
|
||||
let random_part: String = repeat_with(fastrand::alphanumeric).take(50).collect();
|
||||
|
||||
["------", random_part.as_str()].join("")
|
||||
}
|
||||
|
||||
pub fn body_to_string(&self) -> String {
|
||||
|
||||
format!("--{}\r\n{}\r\n--{}--",
|
||||
self.boundary,
|
||||
self.parts.iter()
|
||||
.map(|part| part.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(format!("\r\n--{}\r\n", self.boundary).as_str()),
|
||||
self.boundary)
|
||||
|
||||
}
|
||||
|
||||
pub fn header_to_string(&self) -> String {
|
||||
self.header.to_string()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl fmt::Display for MultiPart<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}\r\n--{}\r\n{}\r\n--{}--",
|
||||
self.header,
|
||||
self.boundary,
|
||||
self.parts.iter()
|
||||
.map(|part| part.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(format!("\r\n--{}\r\n", self.boundary).as_str()),
|
||||
self.boundary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::types::mime::{header::Header, header_line::HeaderLine, part::Part};
|
||||
|
||||
use super::MultiPart;
|
||||
|
||||
|
||||
#[test]
|
||||
fn basic_functionality() {
|
||||
let multi = MultiPart::with_boundary("test-boundary".to_string())
|
||||
.set_header(Header::new()
|
||||
.add(HeaderLine::new("Content-Type").with("multipart/encrypted".to_string()))
|
||||
)
|
||||
.add_part(Part::with_body("body 1".as_bytes().into())
|
||||
.set_header(Header::new()
|
||||
.add(HeaderLine::new("Content-Type")
|
||||
.with("text/plain".to_string())
|
||||
)
|
||||
)
|
||||
)
|
||||
.add_part(Part::with_body("body 2".as_bytes().into())
|
||||
.set_header(Header::new()
|
||||
.add(HeaderLine::new("Content-Type")
|
||||
.with("text/plain".to_string())
|
||||
)
|
||||
)
|
||||
);
|
||||
assert_eq!(multi.to_string(),"Content-Type: multipart/encrypted\r\n\r\n--test-boundary\r\nContent-Type: text/plain\r\n\r\nbody 1\r\n--test-boundary\r\nContent-Type: text/plain\r\n\r\nbody 2\r\n--test-boundary--");
|
||||
assert_eq!(multi.body_to_string(),"--test-boundary\r\nContent-Type: text/plain\r\n\r\nbody 1\r\n--test-boundary\r\nContent-Type: text/plain\r\n\r\nbody 2\r\n--test-boundary--");
|
||||
assert_eq!(multi.header_to_string(),"Content-Type: multipart/encrypted\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Mime part
|
||||
|
||||
use std::fmt;
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::header::Header;
|
||||
|
||||
pub struct Part<'p> {
|
||||
header: Header<'p>,
|
||||
body: Bytes
|
||||
}
|
||||
|
||||
impl<'p> Part<'p> {
|
||||
|
||||
pub fn with_body(body: Bytes) -> Self {
|
||||
Part {
|
||||
header: Header::new(),
|
||||
body: body
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_body(body: Bytes) -> Self {
|
||||
Part {
|
||||
header: Header::new(),
|
||||
body: body
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_header(mut self, header: Header<'p>) -> Self {
|
||||
self.header = header;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Part<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", [
|
||||
self.header.to_string(),
|
||||
String::from_utf8_lossy(&self.body.to_vec()[..]).into()
|
||||
].join("\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::mime::header_line::HeaderLine;
|
||||
|
||||
#[test]
|
||||
fn basic_functionality() {
|
||||
let body = Bytes::from("this is a body");
|
||||
let part = Part::with_body(body)
|
||||
.set_header(Header::new()
|
||||
.add(HeaderLine::new("Content-Type")
|
||||
.with("text/plain".to_string())
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(part.to_string(), "Content-Type: text/plain\r\n\r\nthis is a body");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ pub mod mail_context;
|
||||
pub mod errors;
|
||||
pub mod recipient;
|
||||
pub mod introducer;
|
||||
|
||||
pub mod mime;
|
||||
pub mod cheader;
|
||||
|
||||
Reference in New Issue
Block a user