diff options
author | Alexandros Frantzis <alf82@freemail.gr> | 2019-10-01 23:06:38 +0300 |
---|---|---|
committer | Alexandros Frantzis <alf82@freemail.gr> | 2019-10-01 23:36:26 +0300 |
commit | a054789ddb60ed1fab26e6d4e6bd36ed926273f1 (patch) | |
tree | bd3caad78a0e377816b78276889301d9276344b9 |
Initial public release
-rw-r--r-- | .github/workflows/build.yml | 19 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 24 | ||||
-rw-r--r-- | LICENSE | 373 | ||||
-rw-r--r-- | README.md | 35 | ||||
-rw-r--r-- | examples/normalize.rs | 18 | ||||
-rw-r--r-- | examples/personal-mda.rs | 55 | ||||
-rw-r--r-- | src/decode.rs | 224 | ||||
-rw-r--r-- | src/deliver.rs | 176 | ||||
-rw-r--r-- | src/lib.rs | 373 | ||||
-rw-r--r-- | src/normalize.rs | 477 | ||||
-rw-r--r-- | src/processing.rs | 95 | ||||
-rw-r--r-- | src/regex.rs | 114 | ||||
-rw-r--r-- | tests/test_charset.rs | 104 | ||||
-rw-r--r-- | tests/test_deliver.rs | 86 | ||||
-rw-r--r-- | tests/test_encoded_words.rs | 78 | ||||
-rw-r--r-- | tests/test_encoding.rs | 136 | ||||
-rw-r--r-- | tests/test_fields.rs | 135 | ||||
-rw-r--r-- | tests/test_processing.rs | 39 | ||||
-rw-r--r-- | tests/test_regex.rs | 185 |
20 files changed, 2748 insertions, 0 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4b0e23f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,19 @@ +name: build + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..33b0b15 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "mda" +version = "0.1.0" +authors = ["Alexandros Frantzis <alf82@freemail.gr>"] +edition = "2018" +description = "A library for creating custom Mail Delivery Agents" +license = "MPL-2.0" +repository = "https://github.com/afrantzis/mda-rs" +documentation = "https://docs.rs/mda" +homepage = "https://github.com/afrantzis/mda-rs" +readme = "README.md" +categories = ["email"] +exclude = ["/.github/**"] + +[dependencies] +regex = "1" +libc = "0.2" +gethostname = "0.2" +memchr = "2.2" +charset = "0.1" +lazy_static = "1.4" + +[dev-dependencies] +tempfile = "3" @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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 http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f757ae6 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +mda-rs +====== + +mda-rs is a Rust library for writing custom Mail Deliver Agents. + +![](https://github.com/afrantzis/mda-rs/workflows/build/badge.svg) + +### Documentation + +The detailed module documentation, including code examples for all features, +can be found at [https://docs.rs/mda](https://docs.rs/mda). + +### Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +mda = "0.1" +``` + +If you are using Rust 2015 add the following to your crate root file (Rust 2018 +doesn't require this): + +```rust +extern crate mda; +``` + +See [examples/personal-mda.rs](examples/personal-mda.rs) for an example that +uses mda-rs. + +### License + +This project is licensed under the Mozilla Public License Version 2.0 +([LICENSE](LICENSE) or https://www.mozilla.org/en-US/MPL/2.0/). diff --git a/examples/normalize.rs b/examples/normalize.rs new file mode 100644 index 0000000..359250a --- /dev/null +++ b/examples/normalize.rs @@ -0,0 +1,18 @@ +// 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 + +//! Writes out the normalized form of an email. + +use std::io::{self, Write}; +use mda::{Email, Result}; + +fn main() -> Result<()> { + let email = Email::from_stdin()?; + io::stdout().lock().write_all(email.data())?; + Ok(()) +} diff --git a/examples/personal-mda.rs b/examples/personal-mda.rs new file mode 100644 index 0000000..aa26431 --- /dev/null +++ b/examples/personal-mda.rs @@ -0,0 +1,55 @@ +// 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 + +//! An example of a custom MDA. + +use std::path::PathBuf; + +use mda::{Email, EmailRegex, Result, DeliveryDurability}; + +fn main() -> Result<()> { + // Just some random path to make it highly unlikely that this example will + // indvertently mess up something. + let root = PathBuf::from("/tmp/my-personal-mail-96f29eb6375cfa37"); + + // If we are sure bogofilter is available, the below can be better written as: + // let mut email = Email::from_stdin_filtered(&["/usr/bin/bogofilter", "-ep"])?; + let mut email = Email::from_stdin()?; + if let Ok(new_email) = email.filter(&["/usr/bin/bogofilter", "-ep"]) { + email = new_email; + } + + // Quicker (but possibly less durable) delivery. + email.set_delivery_durability(DeliveryDurability::FileSyncOnly); + + let from = email.header_field("From").unwrap_or(""); + let bogosity = email.header_field("X-Bogosity").unwrap_or(""); + + if bogosity.contains("Spam, tests=bogofilter") || + from.contains("@banneddomain.com") { + email.deliver_to_maildir(root.join("spam"))?; + return Ok(()); + } + + let cc = email.header_field("Cc").unwrap_or(""); + let to = email.header_field("To").unwrap_or(""); + + if to.contains("myworkemail@example.com") || + cc.contains("myworkemail@example.com") { + if email.body().search("URGENCY RATING: (CRITICAL|URGENT)")? { + email.deliver_to_maildir(root.join("inbox/myemail/urgent"))?; + } else { + email.deliver_to_maildir(root.join("inbox/myemail/normal"))?; + } + return Ok(()); + } + + email.deliver_to_maildir(root.join("inbox/unsorted"))?; + + Ok(()) +} diff --git a/src/decode.rs b/src/decode.rs new file mode 100644 index 0000000..8004f18 --- /dev/null +++ b/src/decode.rs @@ -0,0 +1,224 @@ +// 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 + +//! Base64 and quoted-printable decoding. + +use crate::Result; + +const PAD: u8 = 64; // The pseudo-index of the PAD character. +const INV: u8 = 99; // An invalid index. + +static BASE64_INDICES: &'static [u8] = &[ + // 0 1 2 3 4 5 6 7 8 9 A B C D E F +/* 0 */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* 1 */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* 2 */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, 62, INV, INV, INV, 63, +/* 3 */ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, INV, INV, INV, PAD, INV, INV, +/* 4 */ INV, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, +/* 5 */ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, INV, INV, INV, INV, INV, +/* 6 */ INV, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, +/* 7 */ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, INV, INV, INV, INV, INV, +/* 8 */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* 9 */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* A */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* B */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* C */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* D */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* E */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +/* F */ INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, INV, +]; + +/// Decodes base64 encoded data, appending the decoded data to a Vec<u8>. +/// +/// During decoding all line breaks and invalid characters are ignored. +/// If an error is encountered during decoding, the already decoded data in the +/// output buffer is left intact. It's up to the caller to deal with the partial +/// decoded data in case of failure. +pub fn base64_decode_into_buf(input: &[u8], output: &mut Vec<u8>) -> Result<()> { + let mut num_chars = 0; + let mut cur_triplet = 0; + let mut valid_chars = 0; + + for c in input { + let ci = BASE64_INDICES[*c as usize]; + match ci { + // rfc2045: All line breaks or other characters not + // found in Table 1 must be ignored by decoding software. + INV => continue, + _ if ci < PAD => valid_chars += 1, + _ => {} + } + + cur_triplet = cur_triplet << 6 | ((ci & 0x3f) as u32); + num_chars += 1; + + if num_chars == 4 { + match valid_chars { + 2 => output.push((cur_triplet >> 16) as u8), + 3 => output.extend( + &[(cur_triplet >> 16) as u8, (cur_triplet >> 8) as u8] + ), + 4 => output.extend( + &[(cur_triplet >> 16) as u8, + (cur_triplet >> 8) as u8, + cur_triplet as u8 + ] + ), + _ => return Err("Invalid base64 encoding".into()), + } + + cur_triplet = 0; + num_chars = 0; + valid_chars = 0; + } + } + + // rfc2045: A full encoding quantum is always completed at the end of a body. + if num_chars != 0 { + return Err("Unpadded input".into()); + } + + Ok(()) +} + +/// Converts an ascii byte representing a hex digit to it's numerical value. +fn hexdigit_to_num(mut a: u8) -> Option<u8> { + if a.is_ascii_digit() { + return Some(a - b'0'); + } + + a.make_ascii_lowercase(); + + if a >= b'a' && a <= b'f' { + return Some(a - b'a' + 10); + } + + None +} + +/// Decodes quoted-printable encoded data, appending the decoding data to a +/// Vec<u8>. +/// +/// During decoding all line breaks and invalid characters are ignored. +/// If an error is encountered during decoding, the already decoded data in the +/// output buffer is left intact. It's up to the caller to deal with the partial +/// decoded data in case of failure. +pub fn qp_decode_into_buf(input: &[u8], output: &mut Vec<u8>) -> Result<()> { + let mut iter = input.iter().peekable(); + + 'outer: loop { + loop { + match iter.next() { + Some(b'=') => break, + Some(c) => output.push(*c), + None => break 'outer, + } + } + + // At this point we have encountered a '=', so check + // to see what follows. + if let Some(&first) = iter.next() { + // A CRLF/LF after '=' marks a line continuation, and + // is effectively dropped. + if first == b'\r' { + if iter.peek() == Some(&&b'\n') { + iter.next(); + continue; + } + } else if first == b'\n' { + continue; + } else if let Some(first_num) = hexdigit_to_num(first) { + // A valid pair of hexdigits represent the raw byte value. + if let Some(&&second) = iter.peek() { + if let Some(second_num) = hexdigit_to_num(second) { + output.push(first_num * 16 + second_num); + iter.next(); + continue; + } + } + } + + // Emit the raw sequence if it's not one of the special + // special cases checked above. + output.extend(&[b'=', first]); + } else { + // Last character in the input was an '=', just emit it. + output.push(b'='); + } + } + + + Ok(()) +} + +#[cfg(test)] +mod test_base64 { + use crate::decode::base64_decode_into_buf; + + #[test] + fn decodes_full_length() { + let mut decoded = Vec::new(); + assert!(base64_decode_into_buf("YWJj".as_bytes(), &mut decoded).is_ok()); + assert_eq!(decoded, &[b'a', b'b', b'c']); + } + + #[test] + fn decodes_with_two_padding() { + let mut decoded = Vec::new(); + assert!(base64_decode_into_buf("YWJjZA==".as_bytes(), &mut decoded).is_ok()); + assert_eq!(decoded, &[b'a', b'b', b'c', b'd']); + } + + #[test] + fn decodes_with_one_padding() { + let mut decoded = Vec::new(); + assert!(base64_decode_into_buf("YWJjZGU=".as_bytes(), &mut decoded).is_ok()); + assert_eq!(decoded, &[b'a', b'b', b'c', b'd', b'e']); + } + + #[test] + fn error_with_invalid_paddings() { + let mut decoded = Vec::new(); + assert!(base64_decode_into_buf("YWJj====".as_bytes(), &mut decoded).is_err()); + assert!(base64_decode_into_buf("YWJjZ===".as_bytes(), &mut decoded).is_err()); + assert!(base64_decode_into_buf("====".as_bytes(), &mut decoded).is_err()); + } + + #[test] + fn error_with_unpadded_input() { + let mut decoded = Vec::new(); + assert!(base64_decode_into_buf("YWJjZA=".as_bytes(), &mut decoded).is_err()); + } +} + +#[cfg(test)] +mod test_qp { + use crate::decode::qp_decode_into_buf; + + #[test] + fn decodes_byte() { + let mut decoded = Vec::new(); + assert!(qp_decode_into_buf("a=62c=64".as_bytes(), &mut decoded).is_ok()); + assert_eq!(decoded, &[b'a', b'b', b'c', b'd']); + } + + #[test] + fn decodes_soft_break() { + let mut decoded = Vec::new(); + assert!(qp_decode_into_buf("a=\r\nb=\nc".as_bytes(), &mut decoded).is_ok()); + assert_eq!(decoded, &[b'a', b'b', b'c']); + } + + #[test] + fn invalid_sequences_are_untouched() { + let mut decoded = Vec::new(); + let invalid_sequence = "a=6t= c=".as_bytes(); + assert!(qp_decode_into_buf(invalid_sequence, &mut decoded).is_ok()); + assert_eq!(decoded, invalid_sequence); + } +} diff --git a/src/deliver.rs b/src/deliver.rs new file mode 100644 index 0000000..8ade10f --- /dev/null +++ b/src/deliver.rs @@ -0,0 +1,176 @@ +// 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 + +//! Email delivery functionality. + +use std::fs::{self, File}; +use std::io::ErrorKind; +use std::io::prelude::*; +use std::os::unix::prelude::*; +use std::path::{PathBuf, Path}; +use std::process; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::{DeliveryDurability, Result}; + +use gethostname::gethostname; +use libc; + +/// A generator for likely unique maildir email filenames. +/// +/// Using it as an iterator gets a filename that can be used in a maildir +/// and is likely to be unique. +pub struct EmailFilenameGenerator { + count: usize, + max_seen_unix_time: u64, + hostname: String, +} + +impl EmailFilenameGenerator { + pub fn new() -> Self { + // From https://cr.yp.to/proto/maildir.html: + // "To deal with invalid host names, replace / with \057 and : with \072" + let hostname = + gethostname() + .to_string_lossy() + .into_owned() + .replace("/", r"\057") + .replace(":", r"\072"); + + EmailFilenameGenerator{ + count: 0, + max_seen_unix_time: 0, + hostname: hostname, + } + } +} + +impl Iterator for EmailFilenameGenerator { + type Item = String; + + fn next(&mut self) -> Option<String> { + let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let pid = process::id(); + + if self.max_seen_unix_time < unix_time { + self.max_seen_unix_time = unix_time; + self.count = 0; + } else { + self.count += 1; + } + + Some(format!("{}.{}_{}.{}", unix_time, pid, self.count, self.hostname)) + } < |