[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 detect_network():
p = subprocess.run(['ip', 'addr'], stdout=subprocess.PIPE)
if b'inet 1.2.3.4' in p.stdout:
return 'restricted'
return 'other'
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 = detect_network()
if network == 'restricted' and not host.endswith('restricted.org'):
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()