1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
//! Generate configuration for the postfix mail server.
21
//!
22
//! ## Transport maps (`transport_maps`)
23
//!
24
//! <http://www.postfix.org/postconf.5.html#transport_maps>
25
//!
26
//! ## Local recipient maps (`local_recipient_maps`)
27
//!
28
//! <http://www.postfix.org/postconf.5.html#local_recipient_maps>
29
//!
30
//! ## Relay domains (`relay_domains`)
31
//!
32
//! <http://www.postfix.org/postconf.5.html#relay_domains>
33

            
34
use std::{
35
    borrow::Cow,
36
    convert::TryInto,
37
    fs::OpenOptions,
38
    io::{BufWriter, Read, Seek, Write},
39
    path::{Path, PathBuf},
40
};
41

            
42
use crate::{errors::*, Configuration, Connection, DbVal, MailingList, PostPolicy};
43

            
44
/*
45
transport_maps =
46
    hash:/path-to-mailman/var/data/postfix_lmtp
47
local_recipient_maps =
48
    hash:/path-to-mailman/var/data/postfix_lmtp
49
relay_domains =
50
    hash:/path-to-mailman/var/data/postfix_domains
51
*/
52

            
53
/// Settings for generating postfix configuration.
54
///
55
/// See the struct methods for details.
56
#[derive(Debug, Clone, Deserialize, Serialize)]
57
pub struct PostfixConfiguration {
58
    /// The UNIX username under which the mailpot process who processed incoming
59
    /// mail is launched.
60
    pub user: Cow<'static, str>,
61
    /// The UNIX group under which the mailpot process who processed incoming
62
    /// mail is launched.
63
    pub group: Option<Cow<'static, str>>,
64
    /// The absolute path of the `mailpot` binary.
65
    pub binary_path: PathBuf,
66
    /// The maximum number of `mailpot` processes to launch. Default is `1`.
67
    #[serde(default)]
68
    pub process_limit: Option<u64>,
69
    /// The directory in which the map files are saved.
70
    /// Default is `data_path` from [`Configuration`].
71
    #[serde(default)]
72
    pub map_output_path: Option<PathBuf>,
73
    /// The name of the Postfix service name to use.
74
    /// Default is `mailpot`.
75
    ///
76
    /// A Postfix service is a daemon managed by the postfix process.
77
    /// Each entry in the `master.cf` configuration file defines a single
78
    /// service.
79
    ///
80
    /// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html):
81
    /// <https://www.postfix.org/master.5.html>.
82
    #[serde(default)]
83
    pub transport_name: Option<Cow<'static, str>>,
84
}
85

            
86
impl Default for PostfixConfiguration {
87
1
    fn default() -> Self {
88
1
        Self {
89
1
            user: "user".into(),
90
1
            group: None,
91
1
            binary_path: Path::new("/usr/bin/mailpot").to_path_buf(),
92
1
            process_limit: None,
93
1
            map_output_path: None,
94
1
            transport_name: None,
95
        }
96
1
    }
97
}
98

            
99
impl PostfixConfiguration {
100
    /// Generate service line entry for Postfix's [`master.cf`](https://www.postfix.org/master.5.html) file.
101
5
    pub fn generate_master_cf_entry(&self, config: &Configuration, config_path: &Path) -> String {
102
5
        let transport_name = self.transport_name.as_deref().unwrap_or("mailpot");
103
5
        format!(
104
            "{transport_name} unix - n n - {process_limit} pipe
105
flags=RX user={username}{group_sep}{groupname} directory={{{data_dir}}} argv={{{binary_path}}} -c \
106
             {{{config_path}}} post",
107
5
            username = &self.user,
108
5
            group_sep = if self.group.is_none() { "" } else { ":" },
109
5
            groupname = self.group.as_deref().unwrap_or_default(),
110
5
            process_limit = self.process_limit.unwrap_or(1),
111
5
            binary_path = &self.binary_path.display(),
112
5
            config_path = &config_path.display(),
113
5
            data_dir = &config.data_path.display()
114
        )
115
5
    }
116

            
117
    /// Generate `transport_maps` and `local_recipient_maps` for Postfix.
118
    ///
119
    /// The output must be saved in a plain text file.
120
    /// To make Postfix be able to read them, the `postmap` application must be
121
    /// executed with the path to the map file as its sole argument.
122
    /// `postmap` is usually distributed along with the other Postfix binaries.
123
1
    pub fn generate_maps(
124
        &self,
125
        lists: &[(DbVal<MailingList>, Option<DbVal<PostPolicy>>)],
126
    ) -> String {
127
1
        let transport_name = self.transport_name.as_deref().unwrap_or("mailpot");
128
1
        let mut ret = String::new();
129
1
        ret.push_str("# Automatically generated by mailpot.\n");
130
1
        ret.push_str(
131
            "# Upon its creation and every time it is modified, postmap(1) must be called for the \
132
             changes to take effect:\n",
133
        );
134
1
        ret.push_str("# postmap /path/to/map_file\n\n");
135

            
136
        // [ref:TODO]: add custom addresses if PostPolicy is custom
137
7
        let calc_width = |list: &MailingList, policy: Option<&PostPolicy>| -> usize {
138
3
            let addr = list.address.len();
139
3
            match policy {
140
1
                None => 0,
141
2
                Some(PostPolicy { .. }) => addr + "+request".len(),
142
            }
143
3
        };
144

            
145
1
        let Some(width): Option<usize> =
146
7
            lists.iter().map(|(l, p)| calc_width(l, p.as_deref())).max()
147
        else {
148
            return ret;
149
        };
150

            
151
4
        for (list, policy) in lists {
152
            macro_rules! push_addr {
153
                ($addr:expr) => {{
154
                    let addr = &$addr;
155
                    ret.push_str(addr);
156
                    for _ in 0..(width - addr.len() + 5) {
157
                        ret.push(' ');
158
                    }
159
                    ret.push_str(transport_name);
160
                    ret.push_str(":\n");
161
                }};
162
            }
163

            
164
3
            match policy.as_deref() {
165
1
                None => log::debug!(
166
                    "Not generating postfix map entry for list {} because it has no post_policy \
167
                     set.",
168
                    list.id
169
                ),
170
1
                Some(PostPolicy { open: true, .. }) => {
171
14
                    push_addr!(list.address);
172
1
                    ret.push('\n');
173
                }
174
                Some(PostPolicy { .. }) => {
175
15
                    push_addr!(list.address);
176
7
                    push_addr!(list.subscription_mailto().address);
177
9
                    push_addr!(list.owner_mailto().address);
178
1
                    ret.push('\n');
179
                }
180
            }
181
        }
182

            
183
        // pop second of the last two newlines
184
1
        ret.pop();
185

            
186
1
        ret
187
1
    }
188

            
189
    /// Save service to Postfix's [`master.cf`](https://www.postfix.org/master.5.html) file.
190
    ///
191
    /// If you wish to do it manually, get the text output from
192
    /// [`PostfixConfiguration::generate_master_cf_entry`] and manually append it to the [`master.cf`](https://www.postfix.org/master.5.html) file.
193
    ///
194
    /// If `master_cf_path` is `None`, the location of the file is assumed to be
195
    /// `/etc/postfix/master.cf`.
196
4
    pub fn save_master_cf_entry(
197
        &self,
198
        config: &Configuration,
199
        config_path: &Path,
200
        master_cf_path: Option<&Path>,
201
    ) -> Result<()> {
202
4
        let new_entry = self.generate_master_cf_entry(config, config_path);
203
4
        let path = master_cf_path.unwrap_or_else(|| Path::new("/etc/postfix/master.cf"));
204

            
205
        // Create backup file.
206
4
        let path_bkp = path.with_extension("cf.bkp");
207
8
        std::fs::copy(path, &path_bkp).context(format!(
208
            "Could not create master.cf backup {}",
209
4
            path_bkp.display()
210
        ))?;
211
4
        log::info!(
212
            "Created backup of {} to {}.",
213
            path.display(),
214
            path_bkp.display()
215
        );
216

            
217
8
        let mut file = OpenOptions::new()
218
            .read(true)
219
            .write(true)
220
            .create(false)
221
            .open(path)
222
4
            .context(format!("Could not open {}", path.display()))?;
223

            
224
4
        let mut previous_content = String::new();
225

            
226
8
        file.rewind()
227
4
            .context(format!("Could not access {}", path.display()))?;
228
8
        file.read_to_string(&mut previous_content)
229
4
            .context(format!("Could not access {}", path.display()))?;
230

            
231
4
        let original_size = previous_content.len();
232

            
233
4
        let lines = previous_content.lines().collect::<Vec<&str>>();
234
4
        let transport_name = self.transport_name.as_deref().unwrap_or("mailpot");
235

            
236
219
        if let Some(line) = lines.iter().find(|l| l.starts_with(transport_name)) {
237
6
            let pos = previous_content.find(line).ok_or_else(|| {
238
                Error::from(ErrorKind::Bug("Unepected logical error.".to_string()))
239
            })?;
240
4
            let end_needle = " argv=";
241
6
            let end_pos = previous_content[pos..]
242
                .find(end_needle)
243
6
                .and_then(|pos2| {
244
6
                    previous_content[(pos + pos2 + end_needle.len())..]
245
                        .find('\n')
246
9
                        .map(|p| p + pos + pos2 + end_needle.len())
247
3
                })
248
                .ok_or_else(|| {
249
                    Error::from(ErrorKind::Bug("Unepected logical error.".to_string()))
250
3
                })?;
251
3
            previous_content.replace_range(pos..end_pos, &new_entry);
252
        } else {
253
1
            previous_content.push_str(&new_entry);
254
1
            previous_content.push('\n');
255
        }
256

            
257
4
        file.rewind()?;
258
4
        if previous_content.len() < original_size {
259
            file.set_len(
260
                previous_content
261
                    .len()
262
                    .try_into()
263
                    .expect("Could not convert usize file size to u64"),
264
            )?;
265
        }
266
4
        let mut file = BufWriter::new(file);
267
8
        file.write_all(previous_content.as_bytes())
268
4
            .context(format!("Could not access {}", path.display()))?;
269
12
        file.flush()
270
4
            .context(format!("Could not access {}", path.display()))?;
271
4
        log::debug!("Saved new master.cf to {}.", path.display(),);
272

            
273
4
        Ok(())
274
4
    }
275

            
276
    /// Generate `transport_maps` and `local_recipient_maps` for Postfix.
277
    ///
278
    /// To succeed the user the command is running under must have write and
279
    /// read access to `postfix_data_directory` and the `postmap` binary
280
    /// must be discoverable in your `PATH` environment variable.
281
    ///
282
    /// `postmap` is usually distributed along with the other Postfix binaries.
283
    pub fn save_maps(&self, config: &Configuration) -> Result<()> {
284
        let db = Connection::open_db(config.clone())?;
285
        let Some(postmap) = find_binary_in_path("postmap") else {
286
            return Err(Error::from(ErrorKind::External(anyhow::Error::msg(
287
                "Could not find postmap binary in PATH.",
288
            ))));
289
        };
290
        let lists = db.lists()?;
291
        let lists_post_policies = lists
292
            .into_iter()
293
            .map(|l| {
294
                let pk = l.pk;
295
                Ok((l, db.list_post_policy(pk)?))
296
            })
297
            .collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
298
        let content = self.generate_maps(&lists_post_policies);
299
        let path = self
300
            .map_output_path
301
            .as_deref()
302
            .unwrap_or(&config.data_path)
303
            .join("mailpot_postfix_map");
304
        let mut file = BufWriter::new(
305
            OpenOptions::new()
306
                .read(true)
307
                .write(true)
308
                .create(true)
309
                .truncate(true)
310
                .open(&path)
311
                .context(format!("Could not open {}", path.display()))?,
312
        );
313
        file.write_all(content.as_bytes())
314
            .context(format!("Could not write to {}", path.display()))?;
315
        file.flush()
316
            .context(format!("Could not write to {}", path.display()))?;
317

            
318
        let output = std::process::Command::new("sh")
319
            .arg("-c")
320
            .arg(&format!("{} {}", postmap.display(), path.display()))
321
            .output()
322
            .with_context(|| {
323
                format!(
324
                    "Could not execute `postmap` binary in path {}",
325
                    postmap.display()
326
                )
327
            })?;
328
        if !output.status.success() {
329
            use std::os::unix::process::ExitStatusExt;
330
            if let Some(code) = output.status.code() {
331
                return Err(Error::from(ErrorKind::External(anyhow::Error::msg(
332
                    format!(
333
                        "{} exited with {}.\nstderr was:\n---{}---\nstdout was\n---{}---\n",
334
                        code,
335
                        postmap.display(),
336
                        String::from_utf8_lossy(&output.stderr),
337
                        String::from_utf8_lossy(&output.stdout)
338
                    ),
339
                ))));
340
            } else if let Some(signum) = output.status.signal() {
341
                return Err(Error::from(ErrorKind::External(anyhow::Error::msg(
342
                    format!(
343
                        "{} was killed with signal {}.\nstderr was:\n---{}---\nstdout \
344
                         was\n---{}---\n",
345
                        signum,
346
                        postmap.display(),
347
                        String::from_utf8_lossy(&output.stderr),
348
                        String::from_utf8_lossy(&output.stdout)
349
                    ),
350
                ))));
351
            } else {
352
                return Err(Error::from(ErrorKind::External(anyhow::Error::msg(
353
                    format!(
354
                        "{} failed for unknown reason.\nstderr was:\n---{}---\nstdout \
355
                         was\n---{}---\n",
356
                        postmap.display(),
357
                        String::from_utf8_lossy(&output.stderr),
358
                        String::from_utf8_lossy(&output.stdout)
359
                    ),
360
                ))));
361
            }
362
        }
363

            
364
        Ok(())
365
    }
366
}
367

            
368
fn find_binary_in_path(binary_name: &str) -> Option<PathBuf> {
369
    std::env::var_os("PATH").and_then(|paths| {
370
        std::env::split_paths(&paths).find_map(|dir| {
371
            let full_path = dir.join(binary_name);
372
            if full_path.is_file() {
373
                Some(full_path)
374
            } else {
375
                None
376
            }
377
        })
378
    })
379
}
380

            
381
#[test]
382
2
fn test_postfix_generation() -> Result<()> {
383
    use tempfile::TempDir;
384

            
385
    use crate::*;
386

            
387
1
    mailpot_tests::init_stderr_logging();
388

            
389
1
    fn get_smtp_conf() -> melib::smtp::SmtpServerConf {
390
        use melib::smtp::*;
391
1
        SmtpServerConf {
392
1
            hostname: "127.0.0.1".into(),
393
            port: 1025,
394
1
            envelope_from: "foo-chat@example.com".into(),
395
1
            auth: SmtpAuth::None,
396
1
            security: SmtpSecurity::None,
397
1
            extensions: Default::default(),
398
        }
399
1
    }
400

            
401
1
    let tmp_dir = TempDir::new()?;
402

            
403
1
    let db_path = tmp_dir.path().join("mpot.db");
404
1
    let config = Configuration {
405
1
        send_mail: SendMail::Smtp(get_smtp_conf()),
406
        db_path,
407
1
        data_path: tmp_dir.path().to_path_buf(),
408
1
        administrators: vec![],
409
    };
410
1
    let config_path = tmp_dir.path().join("conf.toml");
411
    {
412
1
        let mut conf = OpenOptions::new()
413
            .write(true)
414
            .create(true)
415
            .open(&config_path)?;
416
1
        conf.write_all(config.to_toml().as_bytes())?;
417
1
        conf.flush()?;
418
1
    }
419

            
420
1
    let db = Connection::open_or_create_db(config)?.trusted();
421
1
    assert!(db.lists()?.is_empty());
422

            
423
    // Create three lists:
424
    //
425
    // - One without any policy, which should not show up in postfix maps.
426
    // - One with subscriptions disabled, which would only add the list address in
427
    //   postfix maps.
428
    // - One with subscriptions enabled, which should add all addresses (list,
429
    //   list+{un,}subscribe, etc).
430

            
431
1
    let first = db.create_list(MailingList {
432
        pk: 0,
433
1
        name: "first".into(),
434
1
        id: "first".into(),
435
1
        address: "first@example.com".into(),
436
1
        description: None,
437
1
        topics: vec![],
438
1
        archive_url: None,
439
    })?;
440
1
    assert_eq!(first.pk(), 1);
441
1
    let second = db.create_list(MailingList {
442
        pk: 0,
443
1
        name: "second".into(),
444
1
        id: "second".into(),
445
1
        address: "second@example.com".into(),
446
1
        description: None,
447
1
        topics: vec![],
448
1
        archive_url: None,
449
    })?;
450
1
    assert_eq!(second.pk(), 2);
451
1
    let post_policy = db.set_list_post_policy(PostPolicy {
452
        pk: 0,
453
1
        list: second.pk(),
454
        announce_only: false,
455
        subscription_only: false,
456
        approval_needed: false,
457
        open: true,
458
        custom: false,
459
    })?;
460

            
461
1
    assert_eq!(post_policy.pk(), 1);
462
1
    let third = db.create_list(MailingList {
463
        pk: 0,
464
1
        name: "third".into(),
465
1
        id: "third".into(),
466
1
        address: "third@example.com".into(),
467
1
        description: None,
468
1
        topics: vec![],
469
1
        archive_url: None,
470
    })?;
471
1
    assert_eq!(third.pk(), 3);
472
1
    let post_policy = db.set_list_post_policy(PostPolicy {
473
        pk: 0,
474
1
        list: third.pk(),
475
        announce_only: false,
476
        subscription_only: false,
477
        approval_needed: true,
478
        open: false,
479
        custom: false,
480
    })?;
481

            
482
1
    assert_eq!(post_policy.pk(), 2);
483

            
484
1
    let mut postfix_conf = PostfixConfiguration::default();
485

            
486
3
    let expected_mastercf_entry = format!(
487
        "mailpot unix - n n - 1 pipe
488
flags=RX user={} directory={{{}}} argv={{/usr/bin/mailpot}} -c {{{}}} post\n",
489
1
        &postfix_conf.user,
490
1
        tmp_dir.path().display(),
491
1
        config_path.display()
492
    );
493
1
    assert_eq!(
494
1
        expected_mastercf_entry.trim_end(),
495
1
        postfix_conf.generate_master_cf_entry(db.conf(), &config_path)
496
    );
497

            
498
1
    let lists = db.lists()?;
499
2
    let lists_post_policies = lists
500
        .into_iter()
501
4
        .map(|l| {
502
3
            let pk = l.pk;
503
3
            Ok((l, db.list_post_policy(pk)?))
504
3
        })
505
        .collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
506
1
    let maps = postfix_conf.generate_maps(&lists_post_policies);
507

            
508
2
    let expected = "second@example.com             mailpot:
509

            
510
third@example.com              mailpot:
511
third+request@example.com      mailpot:
512
third+owner@example.com        mailpot:
513
";
514
    assert!(
515
1
        maps.ends_with(expected),
516
        "maps has unexpected contents: has\n{:?}\nand should have ended with\n{:?}",
517
        maps,
518
        expected
519
    );
520

            
521
1
    let master_edit_value = r#"#
522
# Postfix master process configuration file.  For details on the format
523
# of the file, see the master(5) manual page (command: "man 5 master" or
524
# on-line: http://www.postfix.org/master.5.html).
525
#
526
# Do not forget to execute "postfix reload" after editing this file.
527
#
528
# ==========================================================================
529
# service type  private unpriv  chroot  wakeup  maxproc command + args
530
#               (yes)   (yes)   (no)    (never) (100)
531
# ==========================================================================
532
smtp      inet  n       -       y       -       -       smtpd
533
pickup    unix  n       -       y       60      1       pickup
534
cleanup   unix  n       -       y       -       0       cleanup
535
qmgr      unix  n       -       n       300     1       qmgr
536
#qmgr     unix  n       -       n       300     1       oqmgr
537
tlsmgr    unix  -       -       y       1000?   1       tlsmgr
538
rewrite   unix  -       -       y       -       -       trivial-rewrite
539
bounce    unix  -       -       y       -       0       bounce
540
defer     unix  -       -       y       -       0       bounce
541
trace     unix  -       -       y       -       0       bounce
542
verify    unix  -       -       y       -       1       verify
543
flush     unix  n       -       y       1000?   0       flush
544
proxymap  unix  -       -       n       -       -       proxymap
545
proxywrite unix -       -       n       -       1       proxymap
546
smtp      unix  -       -       y       -       -       smtp
547
relay     unix  -       -       y       -       -       smtp
548
        -o syslog_name=postfix/$service_name
549
showq     unix  n       -       y       -       -       showq
550
error     unix  -       -       y       -       -       error
551
retry     unix  -       -       y       -       -       error
552
discard   unix  -       -       y       -       -       discard
553
local     unix  -       n       n       -       -       local
554
virtual   unix  -       n       n       -       -       virtual
555
lmtp      unix  -       -       y       -       -       lmtp
556
anvil     unix  -       -       y       -       1       anvil
557
scache    unix  -       -       y       -       1       scache
558
postlog   unix-dgram n  -       n       -       1       postlogd
559
maildrop  unix  -       n       n       -       -       pipe
560
  flags=DRXhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
561
uucp      unix  -       n       n       -       -       pipe
562
  flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
563
#
564
# Other external delivery methods.
565
#
566
ifmail    unix  -       n       n       -       -       pipe
567
  flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
568
bsmtp     unix  -       n       n       -       -       pipe
569
  flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
570
scalemail-backend unix -       n       n       -       2       pipe
571
  flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
572
mailman   unix  -       n       n       -       -       pipe
573
  flags=FRX user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py ${nexthop} ${user}
574
"#;
575

            
576
1
    let path = tmp_dir.path().join("master.cf");
577
    {
578
1
        let mut mastercf = OpenOptions::new().write(true).create(true).open(&path)?;
579
1
        mastercf.write_all(master_edit_value.as_bytes())?;
580
1
        mastercf.flush()?;
581
1
    }
582
1
    postfix_conf.save_master_cf_entry(db.conf(), &config_path, Some(&path))?;
583
1
    let mut first = String::new();
584
    {
585
1
        let mut mastercf = OpenOptions::new()
586
            .write(false)
587
            .read(true)
588
            .create(false)
589
            .open(&path)?;
590
1
        mastercf.read_to_string(&mut first)?;
591
1
    }
592
    assert!(
593
1
        first.ends_with(&expected_mastercf_entry),
594
        "edited master.cf has unexpected contents: has\n{:?}\nand should have ended with\n{:?}",
595
        first,
596
        expected_mastercf_entry
597
    );
598

            
599
    // test that a smaller entry can be successfully replaced
600

            
601
1
    postfix_conf.user = "nobody".into();
602
1
    postfix_conf.save_master_cf_entry(db.conf(), &config_path, Some(&path))?;
603
1
    let mut second = String::new();
604
    {
605
1
        let mut mastercf = OpenOptions::new()
606
            .write(false)
607
            .read(true)
608
            .create(false)
609
            .open(&path)?;
610
1
        mastercf.read_to_string(&mut second)?;
611
1
    }
612
2
    let expected_mastercf_entry = format!(
613
        "mailpot unix - n n - 1 pipe
614
flags=RX user=nobody directory={{{}}} argv={{/usr/bin/mailpot}} -c {{{}}} post\n",
615
1
        tmp_dir.path().display(),
616
1
        config_path.display()
617
    );
618
    assert!(
619
1
        second.ends_with(&expected_mastercf_entry),
620
        "doubly edited master.cf has unexpected contents: has\n{:?}\nand should have ended \
621
         with\n{:?}",
622
        second,
623
        expected_mastercf_entry
624
    );
625
    // test that a larger entry can be successfully replaced
626
1
    postfix_conf.user = "hackerman".into();
627
1
    postfix_conf.save_master_cf_entry(db.conf(), &config_path, Some(&path))?;
628
1
    let mut third = String::new();
629
    {
630
1
        let mut mastercf = OpenOptions::new()
631
            .write(false)
632
            .read(true)
633
            .create(false)
634
            .open(&path)?;
635
1
        mastercf.read_to_string(&mut third)?;
636
1
    }
637
2
    let expected_mastercf_entry = format!(
638
        "mailpot unix - n n - 1 pipe
639
flags=RX user=hackerman directory={{{}}} argv={{/usr/bin/mailpot}} -c {{{}}} post\n",
640
1
        tmp_dir.path().display(),
641
1
        config_path.display(),
642
    );
643
    assert!(
644
1
        third.ends_with(&expected_mastercf_entry),
645
        "triply edited master.cf has unexpected contents: has\n{:?}\nand should have ended \
646
         with\n{:?}",
647
        third,
648
        expected_mastercf_entry
649
    );
650

            
651
    // test that if groupname is given it is rendered correctly.
652
1
    postfix_conf.group = Some("nobody".into());
653
1
    postfix_conf.save_master_cf_entry(db.conf(), &config_path, Some(&path))?;
654
1
    let mut fourth = String::new();
655
    {
656
1
        let mut mastercf = OpenOptions::new()
657
            .write(false)
658
            .read(true)
659
            .create(false)
660
            .open(&path)?;
661
1
        mastercf.read_to_string(&mut fourth)?;
662
1
    }
663
2
    let expected_mastercf_entry = format!(
664
        "mailpot unix - n n - 1 pipe
665
flags=RX user=hackerman:nobody directory={{{}}} argv={{/usr/bin/mailpot}} -c {{{}}} post\n",
666
1
        tmp_dir.path().display(),
667
1
        config_path.display(),
668
    );
669
    assert!(
670
1
        fourth.ends_with(&expected_mastercf_entry),
671
        "fourthly edited master.cf has unexpected contents: has\n{:?}\nand should have ended \
672
         with\n{:?}",
673
        fourth,
674
        expected_mastercf_entry
675
    );
676

            
677
1
    Ok(())
678
3
}