commit
bc5fbd6ba9
180 files changed
+63750
No files matched your search
@@ -0,0 +1,167 @@
|
||||
//! SMTP data types.
|
||||
|
||||
/// Parameters needed to establish an SMTP connection.
|
||||
/// Passed explicitly to every trait method so the trait stays stateless.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpCredentials {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub tls: bool,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpSendResult {
|
||||
pub message_id: String,
|
||||
pub rfc822: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A message ready to be sent via SMTP.
|
||||
#[derive(Clone)]
|
||||
pub struct SendableMessage {
|
||||
/// Sender email address.
|
||||
pub from: String,
|
||||
/// Primary recipients.
|
||||
pub to: Vec<String>,
|
||||
/// CC recipients.
|
||||
pub cc: Vec<String>,
|
||||
/// BCC recipients.
|
||||
pub bcc: Vec<String>,
|
||||
/// Optional Reply-To mailbox.
|
||||
pub reply_to: Option<String>,
|
||||
/// Optional organization label from the selected identity.
|
||||
pub organization: Option<String>,
|
||||
/// Subject line.
|
||||
pub subject: String,
|
||||
/// Plain-text body.
|
||||
pub text_body: String,
|
||||
/// Optional HTML body.
|
||||
pub html_body: Option<String>,
|
||||
/// In-Reply-To header value for threading.
|
||||
pub in_reply_to: Option<String>,
|
||||
/// References header value for threading.
|
||||
pub references: Option<String>,
|
||||
/// Address that should receive an MDN/read receipt request.
|
||||
pub read_receipt_to: Option<String>,
|
||||
/// Message priority: `high`, `normal`, or `low`.
|
||||
pub priority: String,
|
||||
/// Administrator-configured headers applied to every outgoing copy.
|
||||
pub extra_headers: Vec<(String, String)>,
|
||||
/// File attachments.
|
||||
pub attachments: Vec<AttachmentData>,
|
||||
/// Optional OpenPGP transformation applied to the MIME entity.
|
||||
pub pgp: Option<PgpSendConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PgpMode {
|
||||
None,
|
||||
Sign,
|
||||
Encrypt,
|
||||
SignEncrypt,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PgpSendConfig {
|
||||
pub mode: PgpMode,
|
||||
pub inline: bool,
|
||||
pub secret_key_armor: Option<String>,
|
||||
pub passphrase: String,
|
||||
pub recipient_public_keys: Vec<String>,
|
||||
}
|
||||
|
||||
/// A single file attachment to include in an outgoing message.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AttachmentData {
|
||||
/// Filename as it should appear to the recipient.
|
||||
pub filename: String,
|
||||
/// MIME content type (e.g. "application/pdf").
|
||||
pub content_type: String,
|
||||
/// Raw file content.
|
||||
pub data: Vec<u8>,
|
||||
/// Optional Content-ID for inline images (referenced via `cid:` in HTML).
|
||||
/// When set, the attachment is treated as an inline image rather than a
|
||||
/// regular file attachment.
|
||||
pub content_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Apply RustBox-managed optional headers shared by SMTP delivery and the
|
||||
/// copy appended to IMAP Sent. Header names are static and values are stripped
|
||||
/// of CR/LF before they reach lettre's raw header API.
|
||||
pub fn apply_optional_headers(
|
||||
builder: lettre::message::MessageBuilder,
|
||||
message: &SendableMessage,
|
||||
) -> lettre::message::MessageBuilder {
|
||||
use lettre::message::header::{HeaderName, HeaderValue};
|
||||
|
||||
let mut builder = apply_optional_header_values(
|
||||
builder,
|
||||
message.read_receipt_to.as_deref(),
|
||||
message.organization.as_deref(),
|
||||
&message.priority,
|
||||
);
|
||||
for (name, value) in &message.extra_headers {
|
||||
if let Ok(name) = HeaderName::new_from_ascii(name.clone()) {
|
||||
let clean_value = value
|
||||
.chars()
|
||||
.map(|ch| if matches!(ch, '\r' | '\n') { ' ' } else { ch })
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string();
|
||||
if !clean_value.is_empty() {
|
||||
builder = builder.raw_header(HeaderValue::new(name, clean_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
/// Apply the optional outgoing headers without requiring a full SendableMessage.
|
||||
/// This is also used for RFC822 draft snapshots appended to IMAP.
|
||||
pub fn apply_optional_header_values(
|
||||
mut builder: lettre::message::MessageBuilder,
|
||||
read_receipt_to: Option<&str>,
|
||||
organization: Option<&str>,
|
||||
priority: &str,
|
||||
) -> lettre::message::MessageBuilder {
|
||||
use lettre::message::header::{HeaderName, HeaderValue};
|
||||
|
||||
fn clean(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.map(|ch| if matches!(ch, '\r' | '\n') { ' ' } else { ch })
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn raw(name: &'static str, value: String) -> HeaderValue {
|
||||
HeaderValue::new(HeaderName::new_from_ascii_str(name), value)
|
||||
}
|
||||
|
||||
if let Some(address) = read_receipt_to.map(clean).filter(|value| !value.is_empty()) {
|
||||
builder = builder.raw_header(raw("Disposition-Notification-To", address));
|
||||
}
|
||||
|
||||
if let Some(organization) = organization.map(clean).filter(|value| !value.is_empty()) {
|
||||
builder = builder.raw_header(raw("Organization", organization));
|
||||
}
|
||||
|
||||
let normalized_priority = priority.trim().to_ascii_lowercase();
|
||||
match normalized_priority.as_str() {
|
||||
"high" => {
|
||||
builder = builder
|
||||
.raw_header(raw("Importance", "high".to_string()))
|
||||
.raw_header(raw("X-Priority", "1 (Highest)".to_string()));
|
||||
}
|
||||
"low" => {
|
||||
builder = builder
|
||||
.raw_header(raw("Importance", "low".to_string()))
|
||||
.raw_header(raw("X-Priority", "5 (Lowest)".to_string()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
Reference in new issue
Block a user