1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
//! A library for writing *milters*: mail filtering applications that can be //! integrated with MTAs (mail servers) over the sendmail milter protocol. //! //! This crate contains the Rust bindings for *libmilter*, that is, the sendmail //! mail filter API. As such, it does not try to hide the nature of that //! venerable C library, but exposes its capabilities faithfully with all its //! quirks. If you have used libmilter before, the functionality exposed through //! the [`Context`] and [`ActionContext`] structs, as well as flags such as //! [`Actions`] will be immediately familiar, though some of the names have been //! adapted. //! //! Once it has started up, a milter application is driven by the underlying //! libmilter C library. This documentation will speak of the ‘milter library’ //! in those cases. //! //! # Usage //! //! To give an idea of how to use this crate, let’s create a milter that counts //! the envelope recipients of a message, and adds a header recording the count. //! //! This simple example demonstrates all important aspects of a milter //! application: handling of SMTP events with callbacks (each envelope //! recipient), storing data in the callback context (the recipient count), and //! finally performing some message modification operation (adding a header). //! //! ```no_run //! use milter::*; //! //! #[on_rcpt(rcpt_callback)] //! fn handle_rcpt(context: Context<u32>, _: Vec<&str>) -> milter::Result<Status> { //! match context.data.borrow_mut()? { //! Some(mut count) => *count += 1, //! None => { //! context.data.replace(1)?; //! } //! } //! //! Ok(Status::Continue) //! } //! //! #[on_eom(eom_callback)] //! fn handle_eom(context: ActionContext<u32>) -> milter::Result<Status> { //! if let Some(count) = context.data.take()? { //! context.add_header("X-Rcpt-Count", &count.to_string())?; //! } //! //! Ok(Status::Continue) //! } //! //! #[on_abort(abort_callback)] //! fn handle_abort(context: Context<u32>) -> Status { //! let _ = context.data.take(); //! //! Status::Continue //! } //! //! fn main() { //! Milter::new("inet:3000@localhost") //! .name("RcptMilter") //! .on_rcpt(rcpt_callback) //! .on_eom(eom_callback) //! .on_abort(abort_callback) //! .actions(Actions::ADD_HEADER) //! .run() //! .expect("milter execution failed"); //! } //! ``` //! //! A milter’s behaviour is implemented as **callback functions** that get //! called as certain events happen during an SMTP conversation. Callback //! functions are marked up with attribute macros. For example, [`on_rcpt`], //! called for each `RCPT TO` command or envelope recipient. //! //! All callback functions return a **response [`Status`]** that determines how //! to proceed after completing the callback. The callbacks in the example all //! return [`Continue`], meaning ‘proceed to the next stage’. //! //! The callback functions are then configured on a **`Milter`** instance in //! `main`. [`Milter`] serves as the entry point to configuring and running a //! milter application. //! //! The example also shows how to **store data in the callback context**. //! Context storage is accessible via a [`DataHandle`] exposed on the `Context` //! struct. A thing to keep in mind is that management of the data’s life cycle //! is not entirely automatic; in order to avoid leaking memory, care must be //! taken to reacquire (and drop) the data before the connection closes. In our //! example this is done in `handle_abort`, implemented just for this purpose. //! //! Finally, the [`on_eom`] end-of-message callback is the place where **actions //! may be applied to a message**. These actions – such as adding a header – can //! be found as methods of [`ActionContext`]. //! //! The example is complete and ready to run. A call to `Milter::run` starts the //! application, passing control to the milter library. A running milter can be //! stopped by sending a termination signal, for example by pressing Control-C. //! //! The remainder of this module documentation discusses some topics to be aware //! of when creating milter applications. //! //! # Callback flow //! //! For milter writing one must have an understanding of the ‘flow’ of callback //! calls. This flow mirrors the succession of events during an SMTP //! conversation. //! //! The callback flow is as follows (when [negotiation] is used, it is the very //! first step, preceding `connect`): //! //! * [`connect`] //! * [`helo`] //! * *for each message:* //! * [`mail`] //! * [`rcpt`]\* //! * [`data`] //! * [`header`]\* //! * [`eoh`] //! * [`body`]\* //! * **[`eom`]** //! * [`close`] //! //! Several messages may be processed in a single connection. When that is the //! case, the message-scoped stages `mail` to `eom` will be traversed //! repeatedly. Among the message-scoped processing steps the ones indicated may //! be executed repeatedly. The message-scoped stages are always bracketed by //! the connection-scoped stages `connect` and `close`. //! //! At any point during processing of a *message* the flow may be diverted to //! [`abort`], in which case the remaining message stages are skipped and //! processing continues at the beginning of the message loop. In any case //! `close` will be called at the very end. //! //! For each stage, a response status returned from the callback determines what //! to do with the entity being processed: whether to continue, accept, or //! reject it. Only at the `eom` (end-of-message) stage may message modification //! operations such as adding headers or altering the message body be applied. //! //! Further detail on this and on the high-level design of the milter library //! can be found in its [documentation][milter]. //! //! # Callback resource management //! //! The callback context allows storing connection-local [data]. Indeed, given //! that the milter library may employ multiple threads of execution for //! handling requests, all data shared across callback functions must be //! accessed using that `DataHandle`. //! //! Context data need to be allocated and released at an appropriate place in //! the callback flow. From the previous section it follows that resources may //! logically be connection-scoped or message-scoped. For cleaning up //! message-scoped resources, `eom` and `abort` are the natural stages to do so, //! whereas for connection-scoped resources it is the `close` stage. //! //! Note that callback resource management is not automatic. Take care to //! reacquire and drop any resources stored in the callback context before the //! connection closes. As a rule of thumb, all paths through the callback flow //! must include a final call to [`DataHandle::take`]. Failure to drop the data //! in time causes that memory to leak. //! //! # Safety //! //! As the milter library is written in C, your Rust callback code is ultimately //! always invoked by a foreign, C caller. Thanks to the attribute //! macro-generated conversion layer, your code is safe even in the presence of //! panics: In Rust, panicking across an FFI boundary is undefined behaviour; //! the macro-generated layer catches unwinding panics, and so panicking in user //! code remains safe. //! //! As usual, panic is treated as a fatal error. A panic triggered in a callback //! results in milter shutdown. //! //! A less extreme failure mode can be chosen by wrapping the callback return //! type in [`milter::Result`], for example `milter::Result<Status>` instead of //! `Status`. Then, the `?` operator can be used to propagate unanticipated //! errors out of the callback. An `Err` result corresponds to a [`Tempfail`] //! response and the milter does not shut down. //! //! A further safety concern is the memory leak hazard present in the context’s //! `DataHandle`. This was discussed above. //! //! # Globals //! //! According with the design of the milter library, a milter application is a //! singleton (one and only one instance). Only a single invocation of //! `Milter::run` is allowed to be active at a time per process. Therefore, //! global variables are an acceptable and reasonable thing to have. //! //! Nevertheless, as the milter library may use multiple threads to handle //! callbacks, any use of static items should use an adequate synchronisation //! mechanism. //! //! # Testing //! //! The design of this crate is not well suited for unit testing. //! //! Instead, it is recommended to set up integration-level testing for milter //! applications. For example, the test suite of the milter crate itself uses //! the `miltertest` tool for integration testing. This tool is part of the //! `opendkim-tools` package on Debian and Ubuntu. //! //! [`Context`]: struct.Context.html //! [`ActionContext`]: struct.ActionContext.html //! [`Actions`]: struct.Actions.html //! [`on_rcpt`]: https://docs.rs/milter-callback/0.1.3/milter_callback/attr.on_rcpt.html //! [`Status`]: enum.Status.html //! [`Continue`]: enum.Status.html#variant.Continue //! [`Milter`]: struct.Milter.html //! [`DataHandle`]: struct.DataHandle.html //! [`on_eom`]: https://docs.rs/milter-callback/0.1.3/milter_callback/attr.on_eom.html //! [negotiation]: struct.Milter.html#method.on_negotiate //! [`connect`]: struct.Milter.html#method.on_connect //! [`helo`]: struct.Milter.html#method.on_helo //! [`mail`]: struct.Milter.html#method.on_mail //! [`rcpt`]: struct.Milter.html#method.on_rcpt //! [`data`]: struct.Milter.html#method.on_data //! [`header`]: struct.Milter.html#method.on_header //! [`eoh`]: struct.Milter.html#method.on_eoh //! [`body`]: struct.Milter.html#method.on_body //! [`eom`]: struct.Milter.html#method.on_eom //! [`close`]: struct.Milter.html#method.on_close //! [`abort`]: struct.Milter.html#method.on_abort //! [milter]: https://salsa.debian.org/debian/sendmail/tree/master/libmilter/docs //! [data]: struct.Context.html#structfield.data //! [`DataHandle::take`]: struct.DataHandle.html#method.take //! [`milter::Result`]: type.Result.html //! [`Tempfail`]: enum.Status.html#variant.Tempfail #![doc(html_root_url = "https://docs.rs/milter/0.1.3")] use std::sync::atomic::{AtomicBool, Ordering}; use bitflags::bitflags; use milter_sys as sys; #[doc(hidden)] pub use sys::{sfsistat, SMFICTX}; #[doc(hidden)] pub use milter_callback::*; mod error; mod context; mod runner; pub use crate::error::*; pub use crate::context::*; pub use crate::runner::*; // TODO revisit panic design? // What to do when a callback panics? We have to catch the panic and do // *something* since panicking across an FFI boundary is undefined behaviour. // // We use libmilter’s `stop` function, and since that is a one-way, terminal // operation, it seems fine to also use a tripwire in the form of this static // Boolean flag here. A panic means there is a severe violation of assumptions, // so we really don’t want to continue work. We do not control the worker // threads, that is why we have to prevent callback code from executing by using // a flag. static PANICKED: AtomicBool = AtomicBool::new(false); // Implementation detail, do not use. #[doc(hidden)] pub fn set_panicked(panicked: bool) { PANICKED.store(panicked, Ordering::Relaxed); } // Implementation detail, do not use. #[doc(hidden)] pub fn is_panicked() -> bool { PANICKED.load(Ordering::Relaxed) } bitflags! { /// Flags representing milter actions. /// /// # Examples /// /// ``` /// # use milter::Actions; /// let header_actions = Actions::ADD_HEADER | Actions::REPLACE_HEADER; /// ``` #[derive(Default)] pub struct Actions: u64 { /// Set requested macros. /// /// This flag enables the [`Context::set_requested_macros`] method. /// /// [`Context::set_requested_macros`]: struct.Context.html#method.set_requested_macros const SET_REQUESTED_MACROS = sys::SMFIF_SETSYMLIST; /// Replace the envelope sender (`MAIL FROM` address) of a message. /// /// This flag enables the [`ActionContext::replace_sender`] action. /// /// [`ActionContext::replace_sender`]: struct.ActionContext.html#method.replace_sender const REPLACE_SENDER = sys::SMFIF_CHGFROM; /// Add an envelope recipient (`RCPT TO` address) for a message. /// /// Together with [`ADD_RECIPIENT_EXT`], this flag enables the /// [`ActionContext::add_recipient`] action. /// /// [`ADD_RECIPIENT_EXT`]: #associatedconstant.ADD_RECIPIENT_EXT /// [`ActionContext::add_recipient`]: struct.ActionContext.html#method.add_recipient const ADD_RECIPIENT = sys::SMFIF_ADDRCPT; /// Add an envelope recipient (`RCPT TO` address) for a message, /// including ESMTP arguments. /// /// Together with [`ADD_RECIPIENT`], this flag enables the /// [`ActionContext::add_recipient`] action. /// /// [`ADD_RECIPIENT`]: #associatedconstant.ADD_RECIPIENT /// [`ActionContext::add_recipient`]: struct.ActionContext.html#method.add_recipient const ADD_RECIPIENT_EXT = sys::SMFIF_ADDRCPT_PAR; /// Remove an envelope recipient (`RCPT TO` address) from a message. /// /// This flag enables the [`ActionContext::remove_recipient`] action. /// /// [`ActionContext::remove_recipient`]: struct.ActionContext.html#method.remove_recipient const REMOVE_RECIPIENT = sys::SMFIF_DELRCPT; /// Add a header to a message. /// /// This flag enables the [`ActionContext::add_header`] and /// [`ActionContext::insert_header`] actions. /// /// [`ActionContext::add_header`]: struct.ActionContext.html#method.add_header /// [`ActionContext::insert_header`]: struct.ActionContext.html#method.insert_header const ADD_HEADER = sys::SMFIF_ADDHDRS; /// Replace a header of a message. /// /// This flag enables the [`ActionContext::replace_header`] action. /// /// [`ActionContext::replace_header`]: struct.ActionContext.html#method.replace_header const REPLACE_HEADER = sys::SMFIF_CHGHDRS; /// Replace the body of a message. /// /// This flag enables the [`ActionContext::append_new_body_chunk`] /// action. /// /// [`ActionContext::append_new_body_chunk`]: struct.ActionContext.html#method.append_new_body_chunk const REPLACE_BODY = sys::SMFIF_CHGBODY; /// Quarantine a message. /// /// This flag enables the [`ActionContext::quarantine`] action. /// /// [`ActionContext::quarantine`]: struct.ActionContext.html#method.quarantine const QUARANTINE = sys::SMFIF_QUARANTINE; } } bitflags! { /// Flags representing milter protocol options. /// /// These flags are used during [negotiation]. There are two facets to their /// meaning: `ProtocolOpts` flags denote either what the MTA can do (MTA /// advertises capabilities), or what the milter application wants to do /// (milter requests capabilities). /// /// [negotiation]: https://docs.rs/milter-callback/0.1.3/milter_callback/attr.on_negotiate.html #[derive(Default)] pub struct ProtocolOpts: u64 { /// Do not use the `connect` stage callback. const NO_CONNECT = sys::SMFIP_NOCONNECT; /// Do not use the `helo` stage callback. const NO_HELO = sys::SMFIP_NOHELO; /// Do not use the `mail` stage callback. const NO_MAIL = sys::SMFIP_NOMAIL; /// Do not use the `rcpt` stage callback. const NO_RCPT = sys::SMFIP_NORCPT; /// Do not use the `data` stage callback. const NO_DATA = sys::SMFIP_NODATA; /// Do not use the `header` stage callback. const NO_HEADER = sys::SMFIP_NOHDRS; /// Do not use the `eoh` stage callback. const NO_EOH = sys::SMFIP_NOEOH; /// Do not use the `body` stage callback. const NO_BODY = sys::SMFIP_NOBODY; /// Do not use the `unknown` stage callback. const NO_UNKNOWN = sys::SMFIP_NOUNKNOWN; /// Allow skipping further (repeated) calls to the same callback. const SKIP = sys::SMFIP_SKIP; /// Also send rejected envelope recipients. const REJECTED_RCPT = sys::SMFIP_RCPT_REJ; /// Respond with [`Noreply`] in the `connect` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_CONNECT = sys::SMFIP_NR_CONN; /// Respond with [`Noreply`] in the `helo` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_HELO = sys::SMFIP_NR_HELO; /// Respond with [`Noreply`] in the `mail` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_MAIL = sys::SMFIP_NR_MAIL; /// Respond with [`Noreply`] in the `rcpt` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_RCPT = sys::SMFIP_NR_RCPT; /// Respond with [`Noreply`] in the `data` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_DATA = sys::SMFIP_NR_DATA; /// Respond with [`Noreply`] in the `header` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_HEADER = sys::SMFIP_NR_HDR; /// Respond with [`Noreply`] in the `eoh` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_EOH = sys::SMFIP_NR_EOH; /// Respond with [`Noreply`] in the `body` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_BODY = sys::SMFIP_NR_BODY; /// Respond with [`Noreply`] in the `unknown` stage. /// /// [`Noreply`]: enum.Status.html#variant.Noreply const NOREPLY_UNKNOWN = sys::SMFIP_NR_UNKN; /// Neither trim nor add leading space in header values. /// /// Header lines in the canonical form `Name: Value` include leading /// whitespace in the header value, which is stripped by default (or /// added when [adding headers]). With this option that leading space is /// kept exactly as given. /// /// [adding headers]: struct.ActionContext.html#method.add_header const HEADER_LEADING_SPACE = sys::SMFIP_HDR_LEADSPC; } } /// The milter protocol stage. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Stage { /// The `connect` stage. Connect = 0, /// The `helo` stage. Helo = 1, /// The `mail` stage. Mail = 2, /// The `rcpt` stage. Rcpt = 3, /// The `data` stage. Data = 4, /// The `eoh` stage. Eoh = 6, /// The `eom` stage. Eom = 5, } /// Callback response status. /// /// A response status is returned from all milter callbacks. It controls whether /// and how processing of some entity is to proceed; ‘entity’ signifies either /// connection, message, or recipient, according to which protocol stage the /// status is returned from. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Status { /// Proceed to the next stage. This is the neutral, default response. Continue = 0, /// Reject the entity being processed. Reject = 1, /// Reject the entity being processed with a temporary failure (client may /// retry). Tempfail = 4, /// Accept the entity being processed but discard the message. Discard = 2, /// Accept the entity being processed. Accept = 3, /// Do not send a reply. When negotiated for a particular stage, this status /// must always be used. /// /// This status is only available if it has been [negotiated] beforehand. /// /// [negotiated]: struct.Milter.html#method.on_negotiate Noreply = 7, /// Skip further (repeated) calls to this callback. This is useful in the /// `body` stage, where potentially costly transmission of body content may /// be cut short once the milter has received enough data. /// /// This status is only available if it has been [negotiated] beforehand. /// /// [negotiated]: struct.Milter.html#method.on_negotiate Skip = 8, /// A special status indication used only in [negotiation]: enable all /// [`ProtocolOpts`] the MTA has to offer. /// /// [negotiation]: struct.Milter.html#method.on_negotiate /// [`ProtocolOpts`]: struct.ProtocolOpts.html AllOpts = 10, } impl Default for Status { fn default() -> Self { Self::Continue } } /// Instructs the milter library to exit its event loop, thereby shutting down /// any currently running milter. /// /// A call to `shutdown` performs a graceful termination of a milter. It causes /// a currently executing blocking call to [`Milter::run`] to return. Note that /// this is a **terminal operation which disables any further executions of /// `Milter::run` in this process.** /// /// [`Milter::run`]: struct.Milter.html#method.run pub fn shutdown() { let _ = unsafe { sys::smfi_stop() }; } /// Returns the runtime version of the milter library. /// /// # Examples /// /// ``` /// let (major, minor, patch) = milter::version(); /// /// println!("Milter v{}.{}.{}", major, minor, patch); /// ``` pub fn version() -> (u32, u32, u32) { let (mut major, mut minor, mut patch) = (0, 0, 0); let _ = unsafe { sys::smfi_version(&mut major, &mut minor, &mut patch) }; (major, minor, patch) } /// Sets the trace debug level of the milter library to the given value. /// /// The value range is unspecified, but should fall somewhere between 0 (no /// logging, the default) and 6 (maximum logging volume). pub fn set_debug_level(level: i32) { let _ = unsafe { sys::smfi_setdbg(level) }; } #[cfg(test)] mod tests { use super::*; #[test] fn test_version() { assert_ne!(version(), (0, 0, 0)); } }