[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [msmtp-users] Patch suggestion: msmtp tunnel command



Hi,

The thread (from 2013) to which I'm replying suggests adding a "tunnel" command to msmtp, similar to the tunnel command in isync (mbsync). I'm resurrecting it because I believe the "tunnel" functionality would be very useful.

Martin Lambers wrote:

On Thu, Jan 10, 2013 at 09:06:41PM +0100, Martin Lambers wrote:

> This functionality is also available right now without a > patch, using the following:
> $ ssh -L 2525:remoteSMTP.example:25 -N remoteSSH.example
> $ msmtp --host=localhost --port=2525
> > Is there a clear advantage of having a separate option for > this? (There are users who would like msmtp to be as small as > possible, and they might object to patches that don't add a > clear benefit. Although I think that both your patches are > lightweight.)

The advantage of the suggested command over the above approach is that you avoid the need for a wrapper script to set up and destroy the ssh proxy.

I wrote a msmtp-wrapper script (attached) that implements the above solution. The script was created in two steps. First I wrote an (almost) transparent msmtp wrapper that that reads all msmtp configuration files and then calls the msmtp command passing all the relevant config as command-line arguments. In a second step, I added to this wrapper some (crude) functionality to detect the current network and setup a tunnel if needed.

Even though the script aims to be a proper solution (it's interoperable with normal msmtp), and is already quite long, it has multiple deficiencies:

* TLS certificate checking needs to be disabled, since otherwise the error message "msmtp: TLS certificate verification failed: the certificate owner does not match hostname" appears.

* The local port is baked-in, so sending email in parallel (e.g. from different processes) wouldn't work. This could be resolved, but it's difficult to resolve this reliably.

* Performance is suboptimal, and the whole thing is quite fragile. I.e. the script has to poll to know when the tunnel is ready.

All of these problems would be resolved by adding a "tunnel" command. One would no longer use 'ssh -L' but 'ssh -W' that does not involve a local port.

Is there a chance that the "tunnel" patch gets applied?

Thanks,
Christoph
#!/usr/bin/env python3.6
import sys
import os.path
import time
import argparse
import subprocess
import collections


def error(msg, code=78):
    print('emsmtp:', msg, file=sys.stderr)
    sys.exit(code)


def parse_config(configfile, optional):
    """Parse config file just like msmtp."""
    defaults = {}
    accounts = collections.OrderedDict()
    current = defaults

    try:
        with open(configfile, 'r') as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#'):
                    continue

                line = line.split(maxsplit=1)
                if len(line) == 1:
                    key, = line
                    value = None
                else:
                    key, value = line
                    value = value.strip('"')

                if key == 'defaults':
                    if value is not None:
                        error(
                            'Command "defaults" does not take any parameters.')
                    current = defaults
                elif key == 'account':
                    name = value.split(':')
                    if len(name) == 1:
                        name, = name
                        parents = []
                    else:
                        name, parents = name
                        parents = [p.strip() for p in parents.split(',')]
                    name = name.strip()
                    current = defaults.copy()
                    for p in parents:
                        current.update(accounts[p])
                    accounts[name] = current
                else:
                    key = key.replace('_', '-')
                    current[key] = value
    except FileNotFoundError:
        if not optional:
            raise

    return accounts


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('-a', '--account', help='Force usage of given account')
    parser.add_argument('-C', '--file', help='Set configuration file')
    parser.add_argument('-f', '--from', help='Set envelope from address',
                        dest='from_')
    return parser.parse_known_args()


def get_network():
    p = subprocess.run(['ip', 'addr'], stdout=subprocess.PIPE)
    if b'inet 1.2.3.4' in p.stdout:
        return 'work'

    p = subprocess.run(['arping', '-c1', '192.168.0.254'],
                       stdout=subprocess.PIPE)
    if b'00:aa:bb:cc:dd:ee' in p.stdout:
        return 'home'

    return 'unknown'


def main():
    args, unknown_args = parse_args()
    accounts = parse_config('/etc/msmtprc', True)
    accounts.update(parse_config(args.file or os.path.expanduser('~/.msmtprc'),
                                 not args.file))

    account_name = args.account
    if account_name is None:
        if args.from_ is None:
            account = None
        else:
            for account_name, account in accounts.items():
                if account.get('from') == args.from_:
                    break
    else:
        account = accounts[account_name]
        if nargs.from_ is not None:
            account['from'] = args.from_

    tunnel = None

    if account:
        try:
            host = account['host']
        except:
            error(f'account {account_name}: host not set')

        network = get_network()
        if network == 'work' and not host.endswith('work.com'):
            port = account.get('port')
            if port is None:
                port = '25'
            tunnel = ['ssh', '-NL', f'2525:{host}:{port}', 'mim']
            account['host'] = 'localhost'
            account['port'] = '2525'
            del account['tls-trust-file']
            account['tls-certcheck'] = 'off'

    cmndline = ['/usr/bin/msmtp']
    if account:
        cmndline.extend(f'--{k}={v}' for k, v in account.items())
    cmndline.extend(unknown_args)

    if tunnel:
        tunnel = subprocess.Popen(tunnel)
        for i in range(10):
            time.sleep(0.1)
            p = subprocess.run(['nc', '-z', 'localhost', account['port']])
            if p.returncode == 0:
                break
        else:
            error('Unable to establish tunnel', 75)

    p = subprocess.run(cmndline)

    if tunnel:
        tunnel.terminate()

    sys.exit(p.returncode)


if __name__ == '__main__':
    main()