- JMAP first version.
This commit is contained in:
parent
9f873e0257
commit
263eab2d9c
9 changed files with 436 additions and 38 deletions
100
Cargo.lock
generated
100
Cargo.lock
generated
|
@ -348,6 +348,12 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-attributes"
|
name = "async-attributes"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -635,6 +641,12 @@ dependencies = [
|
||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bufstream"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.18.1"
|
version = "3.18.1"
|
||||||
|
@ -679,6 +691,16 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"encoding_rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.41"
|
version = "0.4.41"
|
||||||
|
@ -1238,10 +1260,14 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"imap",
|
||||||
"jmap-client",
|
"jmap-client",
|
||||||
"log",
|
"log",
|
||||||
|
"mailparse",
|
||||||
"minio",
|
"minio",
|
||||||
|
"native-tls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
@ -1718,6 +1744,31 @@ dependencies = [
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imap"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c617c55def8c42129e0dd503f11d7ee39d73f5c7e01eff55768b3879ff1d107d"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"bufstream",
|
||||||
|
"chrono",
|
||||||
|
"imap-proto",
|
||||||
|
"lazy_static",
|
||||||
|
"native-tls",
|
||||||
|
"nom 5.1.3",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imap-proto"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
|
||||||
|
dependencies = [
|
||||||
|
"nom 5.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "impl-more"
|
name = "impl-more"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
@ -1852,6 +1903,19 @@ dependencies = [
|
||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lexical-core"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.174"
|
version = "0.2.174"
|
||||||
|
@ -1923,6 +1987,17 @@ dependencies = [
|
||||||
"value-bag",
|
"value-bag",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mailparse"
|
||||||
|
version = "0.13.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32"
|
||||||
|
dependencies = [
|
||||||
|
"charset",
|
||||||
|
"data-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-async"
|
name = "maybe-async"
|
||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
|
@ -2053,6 +2128,17 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "5.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
|
||||||
|
dependencies = [
|
||||||
|
"lexical-core",
|
||||||
|
"memchr",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
@ -2375,6 +2461,12 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "5.3.0"
|
version = "5.3.0"
|
||||||
|
@ -2915,7 +3007,7 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
"unicode_categories",
|
"unicode_categories",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3122,6 +3214,12 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
|
@ -10,6 +10,7 @@ repository = "https://alm.pragmatismo.com.br/generalbots/gbserver"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-multipart = "0.6"
|
actix-multipart = "0.6"
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
jmap-client = "0.3.2"
|
jmap-client = "0.3.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
@ -22,3 +23,6 @@ tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||||
|
imap = "2"
|
||||||
|
native-tls = "0.2"
|
||||||
|
mailparse = "0.13"
|
2
src/prompts/business/send-proposal-v0.bas
Normal file
2
src/prompts/business/send-proposal-v0.bas
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Based on this ${history}, generate the response for
|
||||||
|
${to}, signed by ${user}
|
|
@ -6,20 +6,20 @@ company = QUERY "SELECT Company FROM Opportunities WHERE Id = ${opportunity}"
|
||||||
|
|
||||||
doc = FILL template
|
doc = FILL template
|
||||||
|
|
||||||
# Generate email subject and content based on conversation history
|
' Generate email subject and content based on conversation history
|
||||||
subject = REWRITE "Based on this ${history}, generate a subject for a proposal email to ${company}"
|
subject = REWRITE "Based on this ${history}, generate a subject for a proposal email to ${company}"
|
||||||
contents = REWRITE "Based on this ${history}, and ${subject}, generate the e-mail body for ${to}, signed by ${user}, including key points from our proposal"
|
contents = REWRITE "Based on this ${history}, and ${subject}, generate the e-mail body for ${to}, signed by ${user}, including key points from our proposal"
|
||||||
|
|
||||||
# Add proposal to CRM
|
' Add proposal to CRM
|
||||||
CALL "/files/upload", ".gbdrive/Proposals/${company}-proposal.docx", doc
|
CALL "/files/upload", ".gbdrive/Proposals/${company}-proposal.docx", doc
|
||||||
CALL "/files/permissions", ".gbdrive/Proposals/${company}-proposal.docx", "sales-team", "edit"
|
CALL "/files/permissions", ".gbdrive/Proposals/${company}-proposal.docx", "sales-team", "edit"
|
||||||
|
|
||||||
# Record activity in CRM
|
' Record activity in CRM
|
||||||
CALL "/crm/activities/create", opportunity, "email_sent", {
|
CALL "/crm/activities/create", opportunity, "email_sent", {
|
||||||
"subject": subject,
|
"subject": subject,
|
||||||
"description": "Proposal sent to " + company,
|
"description": "Proposal sent to " + company,
|
||||||
"date": NOW()
|
"date": NOW()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send the email
|
' Send the email
|
||||||
CALL "/comm/email/send", to, subject, contents, doc
|
CALL "/comm/email/send", to, subject, contents, doc
|
||||||
|
|
|
@ -55,6 +55,7 @@ if command -v lxc >/dev/null 2>&1; then
|
||||||
rm -rf /tmp/* /var/tmp/*
|
rm -rf /tmp/* /var/tmp/*
|
||||||
|
|
||||||
echo 'Cleaning logs...'
|
echo 'Cleaning logs...'
|
||||||
|
rm -rf /opt/gbo/logs/*
|
||||||
journalctl --vacuum-time=1d 2>/dev/null || true
|
journalctl --vacuum-time=1d 2>/dev/null || true
|
||||||
|
|
||||||
echo 'Cleaning thumbnail cache...'
|
echo 'Cleaning thumbnail cache...'
|
||||||
|
|
|
@ -2,3 +2,4 @@ pub mod config;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
|
pub mod llm;
|
|
@ -5,11 +5,12 @@ pub struct AppConfig {
|
||||||
pub minio: MinioConfig,
|
pub minio: MinioConfig,
|
||||||
pub server: ServerConfig,
|
pub server: ServerConfig,
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
|
pub email: EmailConfig,
|
||||||
|
pub ai: AIConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DatabaseConfig {
|
pub struct DatabaseConfig {
|
||||||
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub server: String,
|
pub server: String,
|
||||||
|
@ -32,6 +33,27 @@ pub struct ServerConfig {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct EmailConfig {
|
||||||
|
pub from: String,
|
||||||
|
pub server: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub reject_unauthorized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AIConfig {
|
||||||
|
pub image_model: String,
|
||||||
|
pub embedding_model: String,
|
||||||
|
pub instance: String,
|
||||||
|
pub key: String,
|
||||||
|
pub llm_model: String,
|
||||||
|
pub version: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
pub fn database_url(&self) -> String {
|
pub fn database_url(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
|
@ -66,6 +88,32 @@ impl AppConfig {
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
bucket: env::var("DRIVE_ORG_PREFIX").unwrap_or_else(|_| "".to_string()),
|
bucket: env::var("DRIVE_ORG_PREFIX").unwrap_or_else(|_| "".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let email = EmailConfig {
|
||||||
|
from: env::var("EMAIL_FROM").expect("EMAIL_FROM not set"),
|
||||||
|
server: env::var("EMAIL_SERVER").expect("EMAIL_SERVER not set"),
|
||||||
|
port: env::var("EMAIL_PORT")
|
||||||
|
.expect("EMAIL_PORT not set")
|
||||||
|
.parse()
|
||||||
|
.expect("EMAIL_PORT must be a number"),
|
||||||
|
username: env::var("EMAIL_USER").expect("EMAIL_USER not set"),
|
||||||
|
password: env::var("EMAIL_PASS").expect("EMAIL_PASS not set"),
|
||||||
|
reject_unauthorized: env::var("EMAIL_REJECT_UNAUTHORIZED")
|
||||||
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ai = AIConfig {
|
||||||
|
image_model: env::var("AI_IMAGE_MODEL").expect("AI_IMAGE_MODEL not set"),
|
||||||
|
embedding_model: env::var("AI_EMBEDDING_MODEL").expect("AI_EMBEDDING_MODEL not set"),
|
||||||
|
instance: env::var("AI_INSTANCE").expect("AI_INSTANCE not set"),
|
||||||
|
key: env::var("AI_KEY").expect("AI_KEY not set"),
|
||||||
|
llm_model: env::var("AI_LLM_MODEL").expect("AI_LLM_MODEL not set"),
|
||||||
|
version: env::var("AI_VERSION").expect("AI_VERSION not set"),
|
||||||
|
endpoint: env::var("AI_ENDPOINT").expect("AI_ENDPOINT not set"),
|
||||||
|
};
|
||||||
|
|
||||||
AppConfig {
|
AppConfig {
|
||||||
minio,
|
minio,
|
||||||
server: ServerConfig {
|
server: ServerConfig {
|
||||||
|
@ -76,6 +124,8 @@ impl AppConfig {
|
||||||
.unwrap_or(8080),
|
.unwrap_or(8080),
|
||||||
},
|
},
|
||||||
database,
|
database,
|
||||||
|
email,
|
||||||
|
ai,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,61 +1,288 @@
|
||||||
use actix_web::web;
|
use std::str::FromStr;
|
||||||
use actix_web::{http::header::ContentType, HttpResponse};
|
|
||||||
|
use actix_web::{error::ErrorInternalServerError, http::header::ContentType, web, HttpResponse};
|
||||||
use jmap_client::{
|
use jmap_client::{
|
||||||
client::Client,
|
client::Client, core::query::Filter, email,
|
||||||
core::query::Filter,
|
identity::Property, mailbox::{self, Role},
|
||||||
email::{self, Property},
|
email::Property as EmailProperty
|
||||||
mailbox::{self, Role},
|
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct EmailResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub subject: String,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
use crate::services::state::AppState;
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
#[actix_web::post("/emails/list")]
|
async fn create_jmap_client(
|
||||||
pub async fn list_emails() -> Result<web::Json<Vec<email::Email>>, actix_web::Error> {
|
state: &web::Data<AppState>,
|
||||||
// 1. Authenticate with JMAP server
|
) -> Result<(Client, String, String, String), actix_web::Error> {
|
||||||
let client = Client::new()
|
let config = state
|
||||||
.credentials(("test@", ""))
|
.config
|
||||||
.connect("https://mail/jmap/")
|
.as_ref()
|
||||||
.await
|
.ok_or_else(|| actix_web::error::ErrorInternalServerError("Configuration not available"))?;
|
||||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
|
||||||
|
|
||||||
|
let client = Client::new()
|
||||||
|
.credentials((
|
||||||
|
config.email.username.as_ref(),
|
||||||
|
config.email.password.as_ref(),
|
||||||
|
))
|
||||||
|
.connect(&config.email.server)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("JMAP connection error: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 2. Get account ID and email
|
||||||
|
let session = client.session();
|
||||||
|
let (account_id, email) = session
|
||||||
|
.accounts()
|
||||||
|
.find_map(|account_id| {
|
||||||
|
let account = session.account(account_id).unwrap();
|
||||||
|
Some((account_id.to_string(), account.name().to_string()))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let identity = client
|
||||||
|
.identity_get("default", Some(vec![Property::Id, Property::Email]))
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("JMAP connection error: {}", e))
|
||||||
|
})?.unwrap();
|
||||||
|
|
||||||
|
let identity_id = identity.id().unwrap();
|
||||||
|
|
||||||
|
println!("Account ID: {}", account_id);
|
||||||
|
println!("Email address: {}", email);
|
||||||
|
println!("IdentityID: {}", identity_id);
|
||||||
|
|
||||||
|
Ok((client, account_id, email, String::from_str(identity_id)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::post("/emails/list")]
|
||||||
|
pub async fn list_emails(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
) -> Result<web::Json<Vec<EmailResponse>>, actix_web::Error> {
|
||||||
|
let (client, account_id, email, identity_id) = create_jmap_client(&state).await?;
|
||||||
|
|
||||||
|
// Get inbox mailbox
|
||||||
let inbox_id = client
|
let inbox_id = client
|
||||||
.mailbox_query(
|
.mailbox_query(
|
||||||
mailbox::query::Filter::role(Role::Inbox).into(),
|
mailbox::query::Filter::role(Role::Inbox).into(),
|
||||||
None::<Vec<_>>,
|
None::<Vec<_>>,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("Failed to query inbox: {}", e))
|
||||||
|
})?
|
||||||
.take_ids()
|
.take_ids()
|
||||||
.pop()
|
.first()
|
||||||
.ok_or_else(|| actix_web::error::ErrorInternalServerError("No inbox found"))?;
|
.ok_or_else(|| actix_web::error::ErrorInternalServerError("Inbox not found"))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
let mut emails = client
|
// Query emails in inbox
|
||||||
|
let email_ids = client
|
||||||
.email_query(
|
.email_query(
|
||||||
Filter::and([email::query::Filter::in_mailbox(inbox_id)]).into(),
|
Filter::and([email::query::Filter::in_mailbox(&inbox_id)]).into(),
|
||||||
[email::query::Comparator::from()].into(),
|
None::<Vec<_>>,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("Failed to query emails: {}", e))
|
||||||
|
})?
|
||||||
|
.take_ids();
|
||||||
|
|
||||||
let email_ids = emails.take_ids();
|
|
||||||
let mut email_list = Vec::new();
|
let mut email_list = Vec::new();
|
||||||
|
|
||||||
for email_id in email_ids {
|
for email_id in email_ids {
|
||||||
if let Some(email) = client
|
// Fetch email details
|
||||||
|
let email = client
|
||||||
.email_get(
|
.email_get(
|
||||||
&email_id,
|
&email_id,
|
||||||
[Property::Subject, Property::Preview, Property::Keywords].into(),
|
[
|
||||||
|
EmailProperty::Id,
|
||||||
|
EmailProperty::Subject,
|
||||||
|
EmailProperty::From,
|
||||||
|
EmailProperty::TextBody,
|
||||||
|
EmailProperty::Preview,
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?
|
.map_err(|e| {
|
||||||
{
|
actix_web::error::ErrorInternalServerError(format!("Failed to get emails: {}", e))
|
||||||
email_list.push(email);
|
})?
|
||||||
}
|
.unwrap();
|
||||||
|
|
||||||
|
let from = email.from().unwrap().first();
|
||||||
|
let (name, email_addr) = if let Some(addr) = from {
|
||||||
|
(
|
||||||
|
addr.name().unwrap_or("Unknown").to_string(),
|
||||||
|
addr.email().to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("Unknown".to_string(), "unknown@example.com".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = email.preview().unwrap_or_default().to_string();
|
||||||
|
|
||||||
|
email_list.push(EmailResponse {
|
||||||
|
id: email.id().unwrap().to_string(),
|
||||||
|
name,
|
||||||
|
email: email_addr,
|
||||||
|
subject: email.subject().unwrap_or_default().to_string(),
|
||||||
|
text,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(web::Json(email_list))
|
Ok(web::Json(email_list))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::post("/emails/suggest-answer/{email_id}")]
|
||||||
|
pub async fn suggest_answer(
|
||||||
|
path: web::Path<String>,
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let email_id = path.into_inner();
|
||||||
|
let (client, account_id, email, identity_id) = create_jmap_client(&state).await?;
|
||||||
|
|
||||||
|
// Fetch the specific email
|
||||||
|
let email = client
|
||||||
|
.email_get(
|
||||||
|
&email_id,
|
||||||
|
[
|
||||||
|
EmailProperty::Id,
|
||||||
|
EmailProperty::Subject,
|
||||||
|
EmailProperty::From,
|
||||||
|
EmailProperty::TextBody,
|
||||||
|
EmailProperty::Preview,
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("Failed to get email: {}", e))
|
||||||
|
})?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| actix_web::error::ErrorNotFound("Email not found"))?;
|
||||||
|
|
||||||
|
let from = email.from().unwrap().first();
|
||||||
|
let sender_info = if let Some(addr) = from {
|
||||||
|
format!("{} <{}>", addr.name().unwrap_or("Unknown"), addr.email())
|
||||||
|
} else {
|
||||||
|
"Unknown sender".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let subject = email.subject().unwrap_or_default();
|
||||||
|
let body_text = email.preview().unwrap_or_default();
|
||||||
|
|
||||||
|
let response = serde_json::json!({
|
||||||
|
"suggested_response": "Thank you for your email. I will review this and get back to you shortly.",
|
||||||
|
"prompt": format!(
|
||||||
|
"Email from: {}\nSubject: {}\n\nBody:\n{}\n\n---\n\nPlease draft a professional response to this email.",
|
||||||
|
sender_info, subject, body_text
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::post("/emails/archive/{email_id}")]
|
||||||
|
pub async fn archive_email(
|
||||||
|
path: web::Path<String>,
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let email_id = path.into_inner();
|
||||||
|
let (client, account_id, email, identity_id) = create_jmap_client(&state).await?;
|
||||||
|
|
||||||
|
// Get Archive mailbox (or create if it doesn't exist)
|
||||||
|
let archive_id = match client
|
||||||
|
.mailbox_query(
|
||||||
|
mailbox::query::Filter::name("Archive").into(),
|
||||||
|
None::<Vec<_>>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(mut result) => {
|
||||||
|
let ids = result.take_ids();
|
||||||
|
if let Some(id) = ids.first() {
|
||||||
|
id.clone()
|
||||||
|
} else {
|
||||||
|
// Create Archive mailbox if it doesn't exist
|
||||||
|
client
|
||||||
|
.mailbox_create("Archive", None::<String>, Role::Archive)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!(
|
||||||
|
"Failed to create archive mailbox: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.take_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(actix_web::error::ErrorInternalServerError(format!(
|
||||||
|
"Failed to query mailboxes: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move email to Archive mailbox
|
||||||
|
client
|
||||||
|
.email_set_mailboxes(&email_id, [&archive_id])
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("Failed to archive email: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"message": "Email archived successfully",
|
||||||
|
"email_id": email_id,
|
||||||
|
"archive_mailbox_id": archive_id
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::post("/emails/send")]
|
||||||
|
pub async fn send_email(
|
||||||
|
payload: web::Json<(String, String, String)>,
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
// Destructure the tuple into individual components
|
||||||
|
let (to, subject, body) = payload.into_inner();
|
||||||
|
|
||||||
|
println!("To: {}", to);
|
||||||
|
println!("Subject: {}", subject);
|
||||||
|
println!("Body: {}", body);
|
||||||
|
|
||||||
|
let (client, account_id, email, identity_id) = create_jmap_client(&state).await?;
|
||||||
|
|
||||||
|
let email_submission = client
|
||||||
|
.email_submission_create("111", account_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("Failed to create email: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let email_id = email_submission.email_id().unwrap();
|
||||||
|
println!("Email-ID: {}", email_id);
|
||||||
|
|
||||||
|
client
|
||||||
|
.email_submission_create(email_id, identity_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
actix_web::error::ErrorInternalServerError(format!("Failed to send email: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::get("/campaigns/{campaign_id}/click/{email}")]
|
#[actix_web::get("/campaigns/{campaign_id}/click/{email}")]
|
||||||
pub async fn save_click(
|
pub async fn save_click(
|
||||||
path: web::Path<(String, String)>,
|
path: web::Path<(String, String)>,
|
||||||
|
|
15
src/services/llm.rs
Normal file
15
src/services/llm.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use actix_web::http::Error;
|
||||||
|
|
||||||
|
|
||||||
|
// You'll need to add this to your AppState
|
||||||
|
pub struct LLM {
|
||||||
|
// Your AI client implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LLM {
|
||||||
|
pub async fn generate_response(&self, prompt: &str) -> Result<String, Error> {
|
||||||
|
// Implement your AI service call here
|
||||||
|
Ok("Suggested response".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue