1mod delivery;
5mod language;
6mod mail;
7mod settings;
8mod smtp;
9mod suppression;
10mod templates;
11#[cfg(feature = "testing")]
12pub mod testing;
13mod text;
14mod transport;
15mod unsubscribe;
16#[cfg(feature = "web")]
17pub mod web;
18
19use std::sync::Arc;
20
21use sofie_core::{Error, Result, Show};
22use sofie_db::worker::Worker;
23use sofie_db::{Db, Dep, Extensions, Jobs, Queued, Resolve, Writes};
24
25#[cfg(feature = "i18n")]
26pub use language::translations;
27pub use mail::{Address, Body, Mail};
28pub use settings::{Security, SmtpSettings};
29pub use suppression::{Reason, Scope, Suppression, scopes, suppress};
30pub use templates::{Template, templates};
31pub use unsubscribe::PATH as UNSUBSCRIBE_PATH;
32
33use settings::{Chosen, Settings};
34use templates::Registered;
35use transport::Transport;
36
37#[derive(Clone)]
39pub struct Setup {
40 from: Address,
41 base_url: Option<String>,
42 transport: Arc<dyn Transport>,
43}
44
45impl Setup {
46 #[doc(hidden)]
47 pub fn from_environment() -> Result<Self> {
48 let settings = Settings::read(sofie_core::environment_variable)?;
49 let transport: Arc<dyn Transport> = match settings.transport {
50 Chosen::Smtp(smtp) => {
51 Arc::new(smtp::Smtp::new(smtp, settings.from.domain().to_owned()))
52 }
53 Chosen::Log { preview } => Arc::new(transport::Logged { preview }),
54 };
55 Ok(Self {
56 from: settings.from,
57 base_url: settings.base_url,
58 transport,
59 })
60 }
61}
62
63#[doc(hidden)]
64pub fn install(db: &Db, setup: &Setup) {
65 sofie_db::provide(db, Arc::new(setup.clone()));
66 sofie_db::name_job(db, delivery::JOB, delivery::deliver);
67}
68
69#[doc(hidden)]
70#[must_use]
71pub fn worker(worker: Worker) -> Worker {
72 worker.job(delivery::JOB, delivery::deliver)
73}
74
75pub struct Mailer {
77 setup: Setup,
78 registered: Vec<Arc<Registered>>,
79 jobs: Jobs,
80 #[cfg(feature = "i18n")]
81 translations: Option<Arc<sofie_engine::Translations>>,
82 #[cfg(feature = "i18n")]
83 locale: Option<sofie_values::Locale>,
84}
85
86impl Mailer {
87 pub fn body(&self, key: &str, data: &(impl Show + ?Sized)) -> Result<Body> {
89 let template = templates::find(&self.registered, key).ok_or_else(|| {
90 Error::internal_message(format!(
91 "there is no mail template `{key}`; write it as `src/mails/<name>.html` \
92 in its module and run `sofie gen`"
93 ))
94 })?;
95 self.render(template, data)
96 }
97
98 pub fn body_from(&self, template: &Template, data: &(impl Show + ?Sized)) -> Result<Body> {
100 let template = templates::find(&self.registered, template.key()).unwrap_or(template);
101 self.render(template, data)
102 }
103
104 pub fn link(&self, path: &str) -> Result<String> {
105 let base = self.setup.base_url.as_deref().ok_or_else(|| {
106 settings::missing_base_url("links in mails start with the app's public URL")
107 })?;
108 if !path.starts_with('/') || path.starts_with("//") {
109 return Err(Error::internal_message(format!(
110 "a link in a mail names a path of this app, not `{path}`"
111 )));
112 }
113 Ok(format!("{base}{path}"))
114 }
115
116 pub fn send(&self, mail: Mail) -> Result<Queued> {
118 let mail = mail.with_default_sender(&self.setup.from).checked()?;
119 self.jobs.queue(delivery::deliver, &mail)
120 }
121}
122
123impl<C: Writes> Resolve<C> for Mailer {
124 async fn resolve(context: &mut C) -> Result<Self> {
125 let setup = installed(context).await?;
126 let registered = Extensions::<Registered>::resolve(context).await?;
127 Ok(Self {
128 setup,
129 registered: registered.to_vec(),
130 jobs: Jobs::resolve(context).await?,
131 #[cfg(feature = "i18n")]
132 translations: Extensions::<language::Spoken>::resolve(context)
133 .await?
134 .first()
135 .map(|spoken| Arc::clone(&spoken.0)),
136 #[cfg(feature = "i18n")]
137 locale: None,
138 })
139 }
140}
141
142async fn installed<C: Writes>(context: &mut C) -> Result<Setup> {
143 let setup = Dep::<Setup>::resolve(context).await.map_err(|_| {
144 Error::internal_message(
145 "mail is not set up: `sofie::run` sets it up from `SOFIE_MAIL_*` with sofie's `mail` \
146 feature, and sofie-testing with its own `mail` feature",
147 )
148 })?;
149 Ok(Setup::clone(&setup))
150}