dkim_milter/
lib.rs

1// DKIM Milter – milter for DKIM signing and verification
2// Copyright © 2022–2023 David Bürgin <dbuergin@gluet.ch>
3//
4// This program is free software: you can redistribute it and/or modify it under
5// the terms of the GNU General Public License as published by the Free Software
6// Foundation, either version 3 of the License, or (at your option) any later
7// version.
8//
9// This program is distributed in the hope that it will be useful, but WITHOUT
10// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12// details.
13//
14// You should have received a copy of the GNU General Public License along with
15// this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! The DKIM Milter application library.
18//!
19//! This library was published to facilitate integration testing of the [DKIM
20//! Milter application][DKIM Milter]. No backwards compatibility guarantees are
21//! made for the public API in this library. Please look into the application
22//! instead.
23//!
24//! [DKIM Milter]: https://crates.io/crates/dkim-milter
25
26mod auth_results;
27mod callbacks;
28mod config;
29mod format;
30mod resolver;
31mod session;
32mod sign;
33mod util;
34mod verify;
35
36pub use crate::{
37    config::{
38        model::{
39            LogDestination, LogLevel, ParseLogDestinationError, ParseLogLevelError,
40            ParseSocketError, ParseSyslogFacilityError, Socket, SyslogFacility,
41        },
42        CliOptions,
43    },
44    resolver::LookupFuture,
45};
46
47use crate::{
48    config::{LogConfig, SessionConfig},
49    resolver::MockLookupTxt,
50};
51use indymilter::IntoListener;
52use log::{error, info, LevelFilter, Log, Metadata, Record, SetLoggerError};
53use std::{
54    error::Error,
55    future::Future,
56    io::{self, stderr, ErrorKind, Write},
57    sync::{Arc, RwLock},
58};
59use tokio::sync::mpsc;
60
61/// The DKIM Milter application name.
62pub const MILTER_NAME: &str = "DKIM Milter";
63
64/// The DKIM Milter version string.
65pub const VERSION: &str = env!("CARGO_PKG_VERSION");
66
67/// Preliminary, partially read configuration, with logger not yet installed.
68pub struct StubConfig {
69    opts: CliOptions,
70    log_config: LogConfig,
71    config_file_content: String,
72}
73
74impl StubConfig {
75    pub async fn read(opts: CliOptions) -> Result<Self, Box<dyn Error + 'static>> {
76        let (log_config, config_file_content) = match LogConfig::read(&opts).await {
77            Ok(config) => config,
78            Err(e) => return Err(Box::new(e)),
79        };
80
81        Ok(Self {
82            opts,
83            log_config,
84            config_file_content,
85        })
86    }
87
88    pub fn install_static_logger(&self) -> Result<(), Box<dyn Error + 'static>> {
89        configure_logging(&self.log_config)?;
90        Ok(())
91    }
92
93    pub async fn read_fully(self) -> Result<Config, Box<dyn Error + 'static>> {
94        let StubConfig { opts, log_config, config_file_content } = self;
95        Config::read_fully(opts, log_config, &config_file_content, None).await
96    }
97
98    pub async fn read_fully_with_lookup(
99        self,
100        lookup: impl Fn(&str) -> LookupFuture + Send + Sync + 'static,
101    ) -> Result<Config, Box<dyn Error + 'static>> {
102        let StubConfig { opts, log_config, config_file_content } = self;
103        let lookup = Arc::new(lookup);
104        Config::read_fully(opts, log_config, &config_file_content, Some(lookup)).await
105    }
106}
107
108pub struct Config {
109    cli_opts: CliOptions,
110    config: config::Config,
111    mock_resolver: Option<MockLookupTxt>,
112}
113
114// Note: `Config::read` is stateful, as it installs a global logger on first
115// use; this logger is active for the rest of the program.
116impl Config {
117    pub async fn read(opts: CliOptions) -> Result<Self, Box<dyn Error + 'static>> {
118        Self::read_internal(opts, None).await
119    }
120
121    pub async fn read_with_lookup(
122        opts: CliOptions,
123        lookup: impl Fn(&str) -> LookupFuture + Send + Sync + 'static,
124    ) -> Result<Self, Box<dyn Error + 'static>> {
125        let lookup = Arc::new(lookup);
126        Self::read_internal(opts, Some(lookup)).await
127    }
128
129    async fn read_internal(
130        opts: CliOptions,
131        mock_resolver: Option<Arc<dyn Fn(&str) -> LookupFuture + Send + Sync>>,
132    ) -> Result<Self, Box<dyn Error + 'static>> {
133        let config = StubConfig::read(opts).await?;
134
135        config.install_static_logger()?;
136
137        // Logging now available; from here on, use logging via log macros.
138
139        let StubConfig { opts, log_config, config_file_content } = config;
140
141        Self::read_fully(opts, log_config, &config_file_content, mock_resolver).await
142    }
143
144    async fn read_fully(
145        opts: CliOptions,
146        log_config: LogConfig,
147        config_file_content: &str,
148        mock_resolver: Option<Arc<dyn Fn(&str) -> LookupFuture + Send + Sync>>,
149    ) -> Result<Self, Box<dyn Error + 'static>> {
150        let config = match config::Config::read_with_log_config(
151            &opts,
152            log_config,
153            config_file_content,
154        )
155        .await
156        {
157            Ok(config) => config,
158            Err(e) => {
159                return Err(Box::new(e));
160            }
161        };
162
163        let mock_resolver = mock_resolver.map(MockLookupTxt::new);
164
165        Ok(Self {
166            cli_opts: opts,
167            config,
168            mock_resolver,
169        })
170    }
171
172    pub fn socket(&self) -> &Socket {
173        &self.config.socket
174    }
175}
176
177fn configure_logging(config: &LogConfig) -> Result<(), Box<dyn Error + 'static>> {
178    let level = match config.log_level {
179        LogLevel::Error => LevelFilter::Error,
180        LogLevel::Warn => LevelFilter::Warn,
181        LogLevel::Info => LevelFilter::Info,
182        LogLevel::Debug => LevelFilter::Debug,
183    };
184
185    match config.log_destination {
186        LogDestination::Syslog => {
187            syslog::init_unix(config.syslog_facility.into(), level).map_err(|e| {
188                io::Error::new(
189                    ErrorKind::Other,
190                    format!("could not initialize syslog: {e}"),
191                )
192            })?;
193        }
194        LogDestination::Stderr => {
195            StderrLog::init(level).map_err(|e| {
196                io::Error::new(
197                    ErrorKind::Other,
198                    format!("could not initialize stderr log: {e}"),
199                )
200            })?;
201        }
202    }
203
204    Ok(())
205}
206
207pub async fn run(
208    listener: impl IntoListener,
209    config: Config,
210    reload: mpsc::Receiver<()>,
211    shutdown: impl Future,
212) -> io::Result<()> {
213    let Config { cli_opts, config, mock_resolver } = config;
214
215    let session_config = match mock_resolver {
216        Some(resolver) => SessionConfig::with_mock_resolver(config, resolver),
217        None => SessionConfig::new(config),
218    };
219    let session_config = Arc::new(RwLock::new(Arc::new(session_config)));
220
221    spawn_reload_task(session_config.clone(), cli_opts, reload);
222
223    let callbacks = callbacks::make_callbacks(session_config);
224    let config = Default::default();
225
226    info!("{MILTER_NAME} {VERSION} starting");
227
228    let result = indymilter::run(listener, callbacks, config, shutdown).await;
229
230    match &result {
231        Ok(()) => info!("{MILTER_NAME} {VERSION} shut down"),
232        Err(e) => error!("{MILTER_NAME} {VERSION} terminated with error: {e}"),
233    }
234
235    result
236}
237
238fn spawn_reload_task(
239    session_config: Arc<RwLock<Arc<SessionConfig>>>,
240    opts: CliOptions,
241    mut reload: mpsc::Receiver<()>,
242) {
243    tokio::spawn(async move {
244        while let Some(()) = reload.recv().await {
245            config::reload(&session_config, &opts).await;
246        }
247    });
248}
249
250/// A minimal log implementation that uses `writeln!` for logging.
251struct StderrLog {
252    level: LevelFilter,
253}
254
255impl StderrLog {
256    fn init<L: Into<LevelFilter>>(level: L) -> Result<(), SetLoggerError> {
257        let level = level.into();
258        log::set_boxed_logger(Box::new(Self { level }))
259            .map(|_| log::set_max_level(level))
260    }
261}
262
263impl Log for StderrLog {
264    fn enabled(&self, metadata: &Metadata) -> bool {
265        metadata.level() <= self.level
266    }
267
268    fn log(&self, record: &Record) {
269        if self.enabled(record.metadata()) {
270            let _ = writeln!(stderr(), "{}", record.args());
271        }
272    }
273
274    fn flush(&self) {}
275}