summaryrefslogtreecommitdiffstats
path: root/src/deliver.rs
blob: 8ade10f0ee46c0aa417b901f43fc2a38c80b161d (plain)
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
// 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))
    }
}

/// A representation of a maildir.
pub struct Maildir {
    root: PathBuf,
    email_filename_gen: Arc<Mutex<EmailFilenameGenerator>>,
}

impl Maildir {
    /// Opens, or creates if it doesn't a exist, a maildir directory structure
    /// at the specified path.
    pub fn open_or_create(
        mailbox: &Path,
        email_filename_gen: Arc<Mutex<EmailFilenameGenerator>>
    ) -> Result<Self> {
        let root = PathBuf::from(mailbox);
        for s in &["tmp", "new", "cur"] {
            let path = root.join(&s);
            fs::create_dir_all(&path)?;
        }

        Ok(Maildir{root, email_filename_gen})
    }

    /// Delivers an email to the maildir by creating a new file with the email data,
    /// and using the specified DeliveryDurability method.
    pub fn deliver(
        &self,
        data: &[u8],
        delivery_durability: DeliveryDurability
    ) -> Result<PathBuf> {
        loop {
            let tmp_dir = self.root.join("tmp");
            let new_dir = self.root.join("new");

            let tmp_email = self.write_email_to_dir(data, &tmp_dir)?;
            let new_email = new_dir.join(
                tmp_email.file_name().ok_or("")?.to_str().ok_or("")?);

            let result = fs::hard_link(&tmp_email, &new_email);
            fs::remove_file(&tmp_email)?;

            match result {
                Ok(_) => {
                    if delivery_durability == DeliveryDurability::FileAndDirSync {
                        File::open(&new_dir)?.sync_all()?;
                        File::open(&tmp_dir)?.sync_all()?;
                    }
                    return Ok(new_email);
                },
                Err(ref err) if err.kind() == ErrorKind::AlreadyExists => {},
                Err(err)  => return Err(err.into()),
            }
        }
    }

    /// Delivers an email to the maildir by hard-linking with an existing file,
    /// and using the specified DeliveryDurability method.
    pub fn deliver_with_hard_link(
        &self,
        src: &Path,
        delivery_durability: DeliveryDurability
    ) -> Result<PathBuf> {
        loop {
            let new_dir = self.root.join("new");
            let new_email = new_dir.join(self.next_email_filename_candidate()?);

            match fs::hard_link(&src, &new_email) {
                Ok(_) => {
                    if delivery_durability == DeliveryDurability::FileAndDirSync {
                        File::open(&new_dir)?.sync_all()?;
                    }
                    return Ok(new_email);
                },
                Err(ref err) if err.kind() == ErrorKind::AlreadyExists => {},
                Err(err)  => return Err(err.into()),
            }
        }
    }

    /// Writes email data to a new file in the specified directory.
    fn write_email_to_dir(&self, data: &[u8], dir: &Path) -> Result<PathBuf> {
        loop {
            let email = dir.join(self.next_email_filename_candidate()?);
            let result = fs::OpenOptions::new()
                        .create_new(true)
                        .write(true)
                        .custom_flags(libc::O_SYNC)
                        .open(&email);

            match result {
                Ok(mut f) => {
                    f.write_all(&data)?;
                    return Ok(email);
                },
                Err(ref err) if err.kind() == ErrorKind::AlreadyExists => {},
                Err(err)  => return Err(err.into()),
            }
        }
    }

    /// Gets the next email filename candidate from the EmailFilenameGenerator.
    fn next_email_filename_candidate(&self) -> Result<String> {
        let mut gen = self.email_filename_gen.lock().map_err(|_| "")?;
        gen.next().ok_or("".into())
    }
}