├── emcli ├── tests │ ├── __init__.py │ └── test_argparse.py ├── __init__.py ├── logger.py ├── storage.py └── emcli.py ├── .gitignore ├── tox.ini ├── setup.py └── readme.md /emcli/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /emcli/__init__.py: -------------------------------------------------------------------------------- 1 | from emcli import main 2 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py27 3 | 4 | [testenv] 5 | deps=nose 6 | commands=nosetests 7 | -------------------------------------------------------------------------------- /emcli/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def get_logger(log_level=logging.INFO): 5 | logger = logging.getLogger(__name__) 6 | logger.setLevel(log_level) 7 | 8 | formatter = logging.Formatter("%(asctime)s [emcli] [%(levelname)s] : %(message)s", "%Y-%m-%d %H:%M:%S") 9 | 10 | handler = logging.StreamHandler() 11 | handler.setFormatter(formatter) 12 | 13 | logger.handlers = [handler] 14 | 15 | return logger 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from setuptools import setup 4 | 5 | setup( 6 | name='emcli', 7 | version='0.2', 8 | author='Mingxing LAI', 9 | author_email='me@mingxinglai.com', 10 | url='https://github.com/lalor/emcli', 11 | description='A email client in terminal', 12 | packages=['emcli'], 13 | install_requires=['yagmail'], 14 | tests_require=['nose', 'tox'], 15 | entry_points={ 16 | 'console_scripts': [ 17 | 'emcli=emcli:main', 18 | ] 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /emcli/storage.py: -------------------------------------------------------------------------------- 1 | class Storage(dict): 2 | """ 3 | A Storage object is like a dictionary except `obj.foo` can be used 4 | in addition to `obj['foo']`. 5 | """ 6 | def __getattr__(self, key): 7 | try: 8 | return self[key] 9 | except KeyError as k: 10 | raise AttributeError(k) 11 | 12 | def __setattr__(self, key, value): 13 | self[key] = value 14 | 15 | def __delattr__(self, key): 16 | try: 17 | del self[key] 18 | except KeyError as k: 19 | raise AttributeError(k) 20 | 21 | def __repr__(self): 22 | return '' 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # A email client in terminal 2 | 3 | ## Introduction 4 | 5 | emcli is inspired by mutt, enable you send email in terminal handy. 6 | 7 | ## Installation 8 | 9 | To install emcli, simply: 10 | 11 | pip install emcli 12 | 13 | Or install emcli from source code: 14 | 15 | git clone https://github.com/lalor/emcli 16 | cd emcli 17 | sudo python setup.py install 18 | 19 | ## Usage 20 | 21 | save emcli settings in `~/.emcli.cnf`: 22 | 23 | $ cat ~/.emcli.cnf 24 | [DEFAULT] 25 | smtp_server = smtp.qq.com 26 | smtp_port = 25 27 | username = 403720692@qq.com 28 | password = abc123 29 | 30 | send email to multiple recipents: 31 | 32 | echo "This email come from terminal" | emcli -s "This is subject" -r joy_lmx@163.com me@mingxinglai.com 33 | 34 | send email with attaches: 35 | 36 | emcli -s "This is subject" -a *.py -r joy_lmx@163.com < /etc/passwd 37 | -------------------------------------------------------------------------------- /emcli/tests/test_argparse.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import tempfile 4 | import unittest 5 | 6 | __author__ = 'hzlaimingxing' 7 | 8 | pardir = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir)) 9 | sys.path.append(pardir) 10 | 11 | from emcli.emcli import get_argparse 12 | 13 | 14 | class TestArgparse(unittest.TestCase): 15 | 16 | def setUp(self): 17 | sys.argv[1:] = [] 18 | 19 | def test_normal_request(self): 20 | args = ['-s', 'subject', '-a', 'a.py', 'b.py', '-r', 'a@163.com', 'b@163.com', '-f', 'config.cnf'] 21 | sys.argv.extend(args) 22 | parser = get_argparse() 23 | 24 | self.assertEqual(parser.subject, 'subject') 25 | self.assertEqual(parser.attaches, ['a.py', 'b.py']) 26 | self.assertEqual(parser.recipients, ['a@163.com', 'b@163.com']) 27 | self.assertEqual(parser.conf, 'config.cnf') 28 | 29 | def test_single_request(self): 30 | args = ['-s', 'subject', '-a', 'a.py', '-r', 'a@163.com'] 31 | sys.argv.extend(args) 32 | parser = get_argparse() 33 | 34 | self.assertEqual(parser.subject, 'subject') 35 | self.assertEqual(parser.attaches, ['a.py']) 36 | self.assertEqual(parser.recipients, ['a@163.com']) 37 | 38 | def test_missing_subject(self): 39 | args = ['-a', 'a.py', 'b.py', '-r', 'a@163.com', 'b@163.com', '-f', 'config.cnf'] 40 | sys.argv.extend(args) 41 | with self.assertRaises(SystemExit): 42 | parser = get_argparse() 43 | 44 | def test_missing_recipients(self): 45 | args = ['-s', 'subject', '-a', 'a.py', 'b.py', '-f', 'config.cnf'] 46 | sys.argv.extend(args) 47 | with self.assertRaises(SystemExit): 48 | parser = get_argparse() 49 | 50 | def test_missing_attaches(self): 51 | args = ['-s', 'subject', '-r', 'a@163.com', 'b@163.com', '-f', 'config.cnf'] 52 | sys.argv.extend(args) 53 | parser = get_argparse() 54 | self.assertEqual(parser.attaches, None) 55 | 56 | def test_missing_conf(self): 57 | args = ['-s', 'subject', '-a', 'a.py', 'b.py', '-r', 'a@163.com', 'b@163.com'] 58 | sys.argv.extend(args) 59 | parser = get_argparse() 60 | self.assertEqual(parser.conf, None) 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /emcli/emcli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | try: 4 | import ConfigParser 5 | except ImportError: 6 | import configparser as ConfigParser 7 | import argparse 8 | import yagmail 9 | 10 | from storage import Storage 11 | from logger import get_logger 12 | 13 | logger = get_logger() 14 | 15 | 16 | def get_argparse(): 17 | parser = argparse.ArgumentParser(description='A email client in terminal') 18 | parser.add_argument('-s', action='store', dest='subject', required=True, help='specify a subject (must be in quotes if it has spaces)') 19 | parser.add_argument('-a', action='store', nargs='*', dest='attaches', required=False, help='attach file(s) to the message') 20 | parser.add_argument('-f', action='store', dest='conf', required=False, help='specify an alternate .emcli.cnf file') 21 | parser.add_argument('-r', action='store', nargs='*', dest='recipients', required=True, help='recipient who you are sending the email to') 22 | parser.add_argument('-v', action='version', version='%(prog)s 0.2') 23 | return parser.parse_args() 24 | 25 | 26 | def get_config_file(config_file): 27 | if config_file is None: 28 | config_file = os.path.expanduser('~/.emcli.cnf') 29 | return config_file 30 | 31 | 32 | def get_meta_from_config(config_file): 33 | config = ConfigParser.SafeConfigParser() 34 | 35 | with open(config_file) as fp: 36 | config.readfp(fp) 37 | 38 | meta = Storage() 39 | for key in ['smtp_server', 'smtp_port', 'username', 'password']: 40 | try: 41 | val = config.get('DEFAULT', key) 42 | except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) as err: 43 | logger.error(err) 44 | raise SystemExit(err) 45 | else: 46 | meta[key] = val 47 | 48 | return meta 49 | 50 | 51 | def get_email_content(): 52 | return sys.stdin.read() 53 | 54 | 55 | def send_email(meta): 56 | content = get_email_content() 57 | body = [content] 58 | if meta.attaches: 59 | body.extend(meta.attaches) 60 | 61 | with yagmail.SMTP(user=meta.username, password=meta.password, 62 | host=meta.smtp_server, port=int(meta.smtp_port)) as yag: 63 | logger.info('ready to send email "{0}" to {1}'.format(meta.subject, meta.recipients)) 64 | ret = yag.send(meta.recipients, meta.subject, body) 65 | 66 | 67 | def main(): 68 | parser = get_argparse() 69 | 70 | config_file = get_config_file(parser.conf) 71 | 72 | if not os.path.exists(config_file): 73 | logger.error('{0} is not exists'.format(config_file)) 74 | raise SystemExit() 75 | else: 76 | meta = get_meta_from_config(config_file) 77 | 78 | meta.subject = parser.subject 79 | meta.recipients = parser.recipients 80 | meta.attaches = parser.attaches 81 | 82 | for attach in meta.attaches: 83 | if not os.path.exists(attach): 84 | logger.error('{0} is not exists'.format(attach)) 85 | raise SystemExit() 86 | 87 | send_email(meta) 88 | 89 | 90 | if __name__ == '__main__': 91 | main() 92 | --------------------------------------------------------------------------------