use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RenderMode {
#[default]
LatestOnly,
ThreadHistory,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LlmContextOptions {
#[serde(default)]
pub render_mode: RenderMode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadMessage {
pub sender: String,
pub timestamp: Option<String>,
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToAction {
pub url: String,
pub text: String,
pub confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessedEmail {
pub body: String,
pub subject: Option<String>,
pub from: Option<Address>,
pub to: Vec<Address>,
pub cc: Vec<Address>,
pub date: Option<String>,
pub rfc_message_id: Option<String>,
pub in_reply_to: Option<Vec<String>>,
pub references: Option<Vec<String>>,
pub signature: Option<String>,
pub raw_body_length: usize,
pub clean_body_length: usize,
pub primary_cta: Option<CallToAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub thread_messages: Vec<ThreadMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Address {
pub name: Option<String>,
pub email: String,
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.name {
Some(name) => write!(f, "{} <{}>", name, self.email),
None => write!(f, "{}", self.email),
}
}
}
impl ProcessedEmail {
pub fn to_llm_context(&self) -> String {
self.to_llm_context_with_options(&LlmContextOptions::default())
}
pub fn to_llm_context_with_options(&self, options: &LlmContextOptions) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(from) = &self.from {
parts.push(format!("FROM: {}", from));
}
if !self.to.is_empty() {
let to_str: Vec<String> = self.to.iter().map(|a| a.to_string()).collect();
parts.push(format!("TO: {}", to_str.join(", ")));
}
if let Some(subject) = &self.subject {
parts.push(format!("SUBJECT: {}", subject));
}
if let Some(date) = &self.date {
parts.push(format!("DATE: {}", date));
}
parts.push("CONTENT:".to_string());
parts.push(self.body.clone());
if matches!(options.render_mode, RenderMode::ThreadHistory)
&& !self.thread_messages.is_empty()
{
parts.push("\n---\n\nTHREAD HISTORY:\n".to_string());
for (i, msg) in self.thread_messages.iter().enumerate() {
if i > 0 {
parts.push("\n---\n".to_string());
}
let header = if let Some(ts) = &msg.timestamp {
format!("[{}] {}", ts, msg.sender)
} else {
format!("[?] {}", msg.sender)
};
parts.push(header);
parts.push(msg.body.clone());
}
}
parts.join("\n")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParsedInput {
pub html: Option<String>,
pub text: Option<String>,
pub subject: Option<String>,
pub from: Option<Address>,
#[serde(default)]
pub to: Vec<Address>,
#[serde(default)]
pub cc: Vec<Address>,
pub date: Option<String>,
pub rfc_message_id: Option<String>,
pub in_reply_to: Option<Vec<String>>,
pub references: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PreprocessOptions {
#[serde(default = "default_true")]
pub strip_quotes: bool,
#[serde(default = "default_true")]
pub strip_signature: bool,
#[serde(default)]
pub max_body_length: usize,
}
impl Default for PreprocessOptions {
fn default() -> Self {
Self {
strip_quotes: true,
strip_signature: true,
max_body_length: 0,
}
}
}
fn default_true() -> bool {
true
}
#[derive(Debug)]
pub enum LangmailError {
ParseFailed,
InvalidGmailMessage(String),
BodyRequiresAttachmentFetch {
mime_type: String,
attachment_id: String,
},
}
impl fmt::Display for LangmailError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LangmailError::ParseFailed => write!(f, "Failed to parse email message"),
LangmailError::InvalidGmailMessage(msg) => {
write!(f, "preprocessGmail: {}", msg)
}
LangmailError::BodyRequiresAttachmentFetch {
mime_type,
attachment_id,
} => write!(
f,
"preprocessGmail: {} body part has no inline data (body.attachmentId={:?}). \
Fetch the part via gmail.users.messages.attachments.get and set body.data \
before calling preprocessGmail.",
mime_type, attachment_id
),
}
}
}
impl std::error::Error for LangmailError {}