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`](crate::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
7
        let Some(width): Option<usize> = lists.iter().map(|(l, p)| calc_width(l, p.as_deref())).max() else {
146
            return ret;
147
        };
148

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

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

            
181
        // pop second of the last two newlines
182
1
        ret.pop();
183

            
184
1
        ret
185
1
    }
186

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

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

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

            
222
4
        let mut previous_content = String::new();
223

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

            
229
4
        let original_size = previous_content.len();
230

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

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

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

            
271
4
        Ok(())
272
4
    }
273

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

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

            
360
        Ok(())
361
    }
362
}
363

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

            
377
#[test]
378
2
fn test_postfix_generation() -> Result<()> {
379
    use tempfile::TempDir;
380

            
381
    use crate::*;
382

            
383
1
    mailpot_tests::init_stderr_logging();
384

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

            
397
1
    let tmp_dir = TempDir::new()?;
398

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

            
416
1
    let db = Connection::open_or_create_db(config)?.trusted();
417
1
    assert!(db.lists()?.is_empty());
418

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

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

            
455
1
    assert_eq!(post_policy.pk(), 1);
456
1
    let third = db.create_list(MailingList {
457
        pk: 0,
458
1
        name: "third".into(),
459
1
        id: "third".into(),
460
1
        address: "third@example.com".into(),
461
1
        description: None,
462
1
        archive_url: None,
463
    })?;
464
1
    assert_eq!(third.pk(), 3);
465
1
    let post_policy = db.set_list_post_policy(PostPolicy {
466
        pk: 0,
467
1
        list: third.pk(),
468
        announce_only: false,
469
        subscription_only: false,
470
        approval_needed: true,
471
        open: false,
472
        custom: false,
473
    })?;
474

            
475
1
    assert_eq!(post_policy.pk(), 2);
476

            
477
1
    let mut postfix_conf = PostfixConfiguration::default();
478

            
479
3
    let expected_mastercf_entry = format!(
480
        "mailpot unix - n n - 1 pipe
481
flags=RX user={} directory={{{}}} argv={{/usr/bin/mailpot}} -c {{{}}} post\n",
482
1
        &postfix_conf.user,
483
1
        tmp_dir.path().display(),
484
1
        config_path.display()
485
    );
486
1
    assert_eq!(
487
1
        expected_mastercf_entry.trim_end(),
488
1
        postfix_conf.generate_master_cf_entry(db.conf(), &config_path)
489
    );
490

            
491
1
    let lists = db.lists()?;
492
2
    let lists_post_policies = lists
493
        .into_iter()
494
4
        .map(|l| {
495
3
            let pk = l.pk;
496
3
            Ok((l, db.list_post_policy(pk)?))
497
3
        })
498
        .collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
499
1
    let maps = postfix_conf.generate_maps(&lists_post_policies);
500

            
501
2
    let expected = "second@example.com             mailpot:
502

            
503
third@example.com              mailpot:
504
third+request@example.com      mailpot:
505
third+owner@example.com        mailpot:
506
";
507
1
    assert!(
508
1
        maps.ends_with(expected),
509
        "maps has unexpected contents: has\n{:?}\nand should have ended with\n{:?}",
510
        maps,
511
        expected
512
    );
513

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

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

            
592
    // test that a smaller entry can be successfully replaced
593

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

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

            
670
1
    Ok(())
671
2
}