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
// Copyright 2019 Alexandros Frantzis
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// SPDX-License-Identifier: MPL-2.0

//! Convenience functionality for regex searches of email data.

use std::str;

use regex::bytes::{RegexBuilder, RegexSetBuilder, SetMatches, Captures};

use crate::Result;

/// Trait providing convenience methods for regular expression searching
/// in emails. The trait methods can be use with the byte data returned by
/// the `Email::header`, `Email::body` and `Email::data` methods.
///
/// This trait treats and searches the email contents as bytes. The regular
/// expression parsing is configured for case-insensitive and multi-line
/// search (i.e., `^` and `$` match beginning and end of lines respectively).
///
/// In addition to the single regular expression searching, a method for
/// matching regular expression sets is provided. This can be more
/// efficient than matching multiple regular expressions independently.
///
/// All the trait methods will fail if the regular expression is
/// invalid, or the searched email data isn't valid utf-8.
pub trait EmailRegex {
    /// Returns whether the contents match a regular expression.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use mda::{Email, EmailRegex};
    /// let email = Email::from_stdin()?;
    /// if email.header().search(r"^To:.*me@example.com")? {
    ///     email.deliver_to_maildir("/my/maildir/path")?;
    /// }
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    fn search(&self, regex: &str) -> Result<bool>;

    /// Returns the capture groups matched from a regular expression.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use std::path::Path;
    /// use mda::{Email, EmailRegex};
    /// let email = Email::from_stdin()?;
    /// if let Some(captures) = email.header().search_with_captures(r"^X-Product: name=(\w+)")? {
    ///     let name = std::str::from_utf8(captures.get(1).unwrap().as_bytes()).unwrap();
    ///     email.deliver_to_maildir(Path::new("/my/maildir/").join(name))?;
    /// }
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    fn search_with_captures(&self, regex: &str) -> Result<Option<Captures>>;

    /// Returns the matches from a set of regular expression. This can be
    /// more efficient than matching multiple regular expressions independently.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use mda::{Email, EmailRegex};
    /// let email = Email::from_stdin()?;
    /// let matched_sets = email.header().search_set(
    ///     &[
    ///         r"^To: confidential <confidential@example.com>",
    ///         r"^X-Confidential: true",
    ///     ]
    /// )?;
    /// if matched_sets.matched_any() {
    ///     email.deliver_to_maildir("/my/mail/confidential/")?;
    /// }
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    fn search_set(&self, regex_set: &[&str]) -> Result<SetMatches>;
}

impl EmailRegex for &[u8] {
    fn search(&self, regex: &str) -> Result<bool> {
        Ok(
            RegexBuilder::new(regex)
                .multi_line(true)
                .case_insensitive(true)
                .build()?
                .is_match(self)
        )
    }

    fn search_with_captures(&self, regex: &str) -> Result<Option<Captures>> {
        Ok(
            RegexBuilder::new(regex)
                .multi_line(true)
                .case_insensitive(true)
                .build()?
                .captures(self)
        )
    }

    fn search_set(&self, regex_set: &[&str]) -> Result<SetMatches> {
        Ok(
            RegexSetBuilder::new(regex_set)
                .multi_line(true)
                .case_insensitive(true)
                .build()?
                .matches(self)
        )
    }
}