├── .gitignore ├── Dockerfile ├── LICENSE ├── PYPI_DESCRIPTION.md ├── README.md ├── publish.sh ├── quickmail ├── __init__.py ├── cli.py ├── commands │ ├── __init__.py │ ├── clear.py │ ├── init.py │ ├── send.py │ └── template.py └── utils │ ├── __init__.py │ └── misc.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | # Credentials 131 | /src/credentials.json 132 | /src/*.pickle 133 | 134 | # idea files 135 | .idea/ 136 | 137 | credentials.json 138 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM debian:latest 4 | # LABEL maintainer "Avi Kumar @avikumar15" 5 | 6 | # Set a working directory 7 | WORKDIR /app 8 | 9 | # Copy requirements 10 | COPY requirements.txt requirements.txt 11 | COPY credentials.json credentials.json 12 | 13 | RUN apt-get update && apt-get install -y \ 14 | software-properties-common \ 15 | python3-setuptools \ 16 | python3-pip \ 17 | nano 18 | 19 | RUN pip3 install -r requirements.txt 20 | 21 | RUN pip3 install --upgrade pip 22 | RUN pip3 install quick-mail 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Avi Kumar Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PYPI_DESCRIPTION.md: -------------------------------------------------------------------------------- 1 | Quick Mail CLI 2 | ================= 3 | 4 | A command line interface to send mail without any hassle. 5 | 6 | ## Why this tool? 7 | 8 | Sending last minute mails using conventional tools can get annoying and tiresome. This CLI helps in such situation since it makes sending mail hassle-free and very quick. Use this tool to send mails quickly without leaving your terminal. 9 | 10 | ## Installation 11 | 12 | Install quick-mail from the Python Package Index (PyPi) 13 | 14 | $ pip install quick-mail 15 | 16 | Or manually from this repository. 17 | 18 | ``` 19 | $ git clone https://github.com/avikumar15/quick-mail-cli 20 | 21 | * add line export PYTHONPATH=/path/to/project/quick-email-cli to ~/.bashrc * 22 | 23 | $ cd quick-mail-cli/ 24 | 25 | * activate virtual environment * 26 | 27 | $ pip install -r requirements.txt 28 | $ pip install . 29 | ``` 30 | 31 | Check installation by running 32 | 33 | 34 | ``` 35 | $ quickmail --version 36 | ``` 37 | 38 | ## Usage 39 | 40 | 41 | To use this: 42 | 43 | $ quickmail --help 44 | 45 |

46 | 47 | ``` 48 | usage: quickmail [-h] [-v] {clear,init,send,template} ... 49 | 50 | A command line interface to send mail without any hassle 51 | 52 | positional arguments: 53 | {clear,init,send,template} 54 | clear clear the body of message from local or even the token if 55 | --justdoit argument is added 56 | init initialise token and set your email id 57 | send send the mail 58 | template manage templates of mail body 59 | 60 | optional arguments: 61 | -h, --help show this help message and exit 62 | -v, --version print current cli version 63 | 64 | ``` 65 | 66 | 67 | Create your [OAuth client ID](https://console.developers.google.com/apis/credentials/) and select app type as Desktop App and download the credentials.json file. 68 |

69 | 70 | Then run the init command to authenticate gmail, and generate token. This command is required to be run only once. 71 | 72 | ``` 73 | $ quickmail init 74 | ``` 75 | 76 | Now you are all set. Use the send command to send mail. 77 | 78 | 79 | $ quickmail send --help 80 | 81 |

82 | 83 | ``` 84 | usage: quickmail send [-h] -r RECEIVER -sub SUBJECT [-t TEMPLATE] [-b BODY] 85 | [-a ATTACHMENT] [-l] 86 | 87 | Use the send command to send mail. Body can be passed as an argument, or typed in 88 | a nano shell. Use optional --lessgo command for sending mail without confirmation 89 | 90 | optional arguments: 91 | -h, --help show this help message and exit 92 | -r RECEIVER, --receiver RECEIVER 93 | receiver's email address, eg. '-r "xyz@gmail.com"' 94 | -sub SUBJECT, --subject SUBJECT 95 | email's subject, eg. '-sub "XYZ submission"' 96 | -t TEMPLATE, --template TEMPLATE 97 | template of email body, eg. '-t="assignment_template"' 98 | -b BODY, --body BODY email's body, eg. '-b "Message Body Comes Here"' 99 | -a ATTACHMENT, --attachment ATTACHMENT 100 | email's attachment path, eg. '~/Desktop/XYZ_Endsem.pdf' 101 | -l, --lessgo skip confirmation before sending mail 102 | 103 | ``` 104 | 105 | Body and attachments are optional arguments. Body can be either passed as an argument otherwise it can also be typed in the nano shell (Use -t argument to use a template body). Use the --lessgo (shorthand -l) to skip confirmation of mail, for quicker mail deliveries. 106 | 107 | To clear the cli storage, use the clear command. Use --justdoit (shorthand -j) to even remove the credential and token files from project directory, this extra argument would allow you to change your primary email address. 108 | 109 | 110 | $ quickmail clear --help 111 | 112 |

113 | 114 | ``` 115 | usage: quickmail clear [-h] [-j] 116 | 117 | Use the clear command to clear all email body that are saved in your home 118 | directories. Additionally, pass --justdoit to remove the credential files as well 119 | 120 | optional arguments: 121 | -h, --help show this help message and exit 122 | -j, --justdoit clear storage including the credentials and token 123 | 124 | ``` 125 | 126 | To manage templates use the template command. 127 | 128 | $ quickmail template --help 129 | 130 |

131 | 132 | ``` 133 | usage: quickmail template [-h] {add,listall,edit} ... 134 | 135 | manage mail templates 136 | 137 | positional arguments: 138 | {add,listall,edit} 139 | add add a new template 140 | listall list all templates 141 | edit edit a particular template 142 | 143 | optional arguments: 144 | -h, --help show this help message and exit 145 | 146 | ``` 147 | 148 | Following is a recording of the terminal session which records the usage of `quickmail` from init command till send command. 149 | 150 |

151 | 152 | [![asciicast](https://asciinema.org/a/78mPkSTa0rTK3TXhnkgRDP6RO.svg)](https://asciinema.org/a/78mPkSTa0rTK3TXhnkgRDP6RO) 153 | 154 | ### Improvements and Bugs 155 | 156 | Found any bugs? Or have any suggestions, feel free to open an issue. 157 | 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quick Mail CLI 2 | ================= 3 | 4 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 5 | 6 | [![PyPi version](https://badgen.net/pypi/v/quick-mail/)](https://pypi.org/project/quick-mail/) 7 | [![PyPI download month](https://img.shields.io/pypi/dm/quick-mail.svg)](https://pypi.python.org/pypi/quick-mail/) 8 | 9 | A command line interface to send mail without any hassle. 10 | 11 | ## Why this tool? 12 | 13 | Sending last minute mails using conventional tools can get annoying and tiresome. This CLI helps in such situation since it makes sending mail hassle-free and very quick. Use this tool to send mails quickly without leaving your terminal. 14 | 15 | ## Installation 16 | 17 | 1. Generate your [OAuth client ID](https://console.developers.google.com/apis/credentials/) and select app type as Desktop App and download the credentials.json file. 18 | 19 | 2. Install quick-mail from any of the following methods 20 | 21 | * The Python Package Index (PyPi) 22 | 23 | ``` 24 | $ pip install quick-mail 25 | ``` 26 | 27 | * From the source-code 28 | 29 | ``` 30 | $ git clone https://github.com/avikumar15/quick-mail-cli 31 | 32 | * add line export PYTHONPATH=/path/to/project/quick-email-cli to ~/.bashrc * 33 | 34 | $ cd quick-mail-cli/ 35 | 36 | * activate virtual environment * 37 | 38 | $ pip install -r requirements.txt 39 | $ pip install . 40 | ``` 41 | 42 | 43 | * By creating a docker image 44 | 45 | Clone this repository and add credentials.json to the project root (Same directory as Dockerfile) and run following commands. 46 | 47 | ``` 48 | docker build . 49 | docker run -i -t --network="host" 50 | $ quickmail init credentials.json 51 | $ * authenticate using your mail * 52 | $ exit 53 | docker commit NEW_IMAGE_NAME:NEW_IMAGE_TAG 54 | ``` 55 | 56 | and then subsequently just run 57 | 58 | ``` 59 | docker run -i -t 60 | ``` 61 | 62 | 3. Check installation by running 63 | 64 | ``` 65 | $ quickmail --version 66 | ``` 67 | 68 | ## Usage 69 | 70 | 71 | To use this: 72 | 73 | $ quickmail --help 74 | 75 |

76 | 77 | ``` 78 | usage: quickmail [-h] [-v] {clear,init,send,template} ... 79 | 80 | A command line interface to send mail without any hassle 81 | 82 | positional arguments: 83 | {clear,init,send,template} 84 | clear clear the body of message from local or even the token if 85 | --justdoit argument is added 86 | init initialise token and set your email id 87 | send send the mail 88 | template manage templates of mail body 89 | 90 | optional arguments: 91 | -h, --help show this help message and exit 92 | -v, --version print current cli version 93 | 94 | ``` 95 | 96 |

97 | 98 | Run the init command to authenticate gmail, and generate token. This command is required to be run only once. 99 | 100 | ``` 101 | $ quickmail init 102 | ``` 103 | 104 | Now you are all set. Use the send command to send mail. 105 | 106 | 107 | $ quickmail send --help 108 | 109 |

110 | 111 | ``` 112 | usage: quickmail send [-h] -r RECEIVER -sub SUBJECT [-t TEMPLATE] [-b BODY] 113 | [-a ATTACHMENT] [-l] 114 | 115 | Use the send command to send mail. Body can be passed as an argument, or typed in 116 | a nano shell. Use optional --lessgo command for sending mail without confirmation 117 | 118 | optional arguments: 119 | -h, --help show this help message and exit 120 | -r RECEIVER, --receiver RECEIVER 121 | receiver's email address, eg. '-r "xyz@gmail.com"' 122 | -sub SUBJECT, --subject SUBJECT 123 | email's subject, eg. '-sub "XYZ submission"' 124 | -t TEMPLATE, --template TEMPLATE 125 | template of email body, eg. '-t="assignment_template"' 126 | -b BODY, --body BODY email's body, eg. '-b "Message Body Comes Here"' 127 | -a ATTACHMENT, --attachment ATTACHMENT 128 | email's attachment path, eg. '~/Desktop/XYZ_Endsem.pdf' 129 | -l, --lessgo skip confirmation before sending mail 130 | 131 | ``` 132 | 133 | Body and attachments are optional arguments. Body can be either passed as an argument otherwise it can also be typed in the nano shell (Use -t argument to use a template body). Use the --lessgo (shorthand -l) to skip confirmation of mail, for quicker mail deliveries. 134 | 135 | To clear the cli storage, use the clear command. Use --justdoit (shorthand -j) to even remove the credential and token files from project directory, this extra argument would allow you to change your primary email address. 136 | 137 | 138 | $ quickmail clear --help 139 | 140 |

141 | 142 | ``` 143 | usage: quickmail clear [-h] [-j] 144 | 145 | Use the clear command to clear all email body that are saved in your home 146 | directories. Additionally, pass --justdoit to remove the credential files as well 147 | 148 | optional arguments: 149 | -h, --help show this help message and exit 150 | -j, --justdoit clear storage including the credentials and token 151 | 152 | ``` 153 | 154 | To manage templates use the template command. 155 | 156 | $ quickmail template --help 157 | 158 |

159 | 160 | ``` 161 | usage: quickmail template [-h] {add,listall,edit} ... 162 | 163 | manage mail templates 164 | 165 | positional arguments: 166 | {add,listall,edit} 167 | add add a new template 168 | listall list all templates 169 | edit edit a particular template 170 | 171 | optional arguments: 172 | -h, --help show this help message and exit 173 | 174 | ``` 175 | 176 | Following is a recording of the terminal session which records the usage of `quickmail` from init command till send command. 177 | 178 |

179 | 180 | [![asciicast](https://asciinema.org/a/78mPkSTa0rTK3TXhnkgRDP6RO.svg)](https://asciinema.org/a/78mPkSTa0rTK3TXhnkgRDP6RO) 181 | 182 | ### Improvements and Bugs 183 | 184 | Found any bugs? Or have any suggestions, feel free to open an issue. 185 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 setup.py sdist bdist_wheel 4 | 5 | echo "Build File Created!" 6 | 7 | twine check dist/* 8 | 9 | echo "Checks Passed" 10 | 11 | twine upload dist/* 12 | 13 | echo "Package Uploaded" 14 | -------------------------------------------------------------------------------- /quickmail/__init__.py: -------------------------------------------------------------------------------- 1 | import quickmail.commands 2 | import quickmail.utils 3 | -------------------------------------------------------------------------------- /quickmail/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from quickmail.commands import BaseCommand 4 | 5 | 6 | def execute(): 7 | cli_description = 'A command line interface to send mail without any hassle' 8 | 9 | parser = argparse.ArgumentParser(description=cli_description) 10 | 11 | parser.add_argument('-v', 12 | '--version', 13 | action='version', 14 | version='%(prog)s 1.0.4', 15 | help='print current cli version') 16 | 17 | command = BaseCommand(parser) 18 | args = parser.parse_args() 19 | command.run_command(args) 20 | -------------------------------------------------------------------------------- /quickmail/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from contextlib import suppress 3 | from argparse import ArgumentParser, Namespace 4 | from typing import Dict 5 | 6 | from zope.interface import Interface, implementer 7 | from zope.interface.exceptions import Invalid, MultipleInvalid 8 | 9 | from quickmail.utils.misc import walk_modules, command_dir_path 10 | 11 | 12 | class ICommand(Interface): 13 | 14 | def add_arguments(parser: ArgumentParser) -> None: 15 | pass 16 | 17 | def run_command(args: Namespace) -> None: 18 | pass 19 | 20 | def get_desc(self) -> str: 21 | pass 22 | 23 | 24 | def populate_commands(cls): 25 | 26 | # finding all files inside src.commands, and returning files which have classes inherited from ICommand 27 | for module in walk_modules(command_dir_path): 28 | for obj in vars(module).values(): 29 | with suppress(Invalid, MultipleInvalid): 30 | if ( 31 | inspect.isclass(obj) 32 | # and verifyClass(ICommand, obj) 33 | and obj.__module__ == module.__name__ 34 | and not obj == cls 35 | ): 36 | yield obj 37 | 38 | 39 | @implementer(ICommand) 40 | class BaseCommand: 41 | 42 | def __init__(self, parser: ArgumentParser): 43 | # commands_dict contains a dictionary from the command name to the class that implements it 44 | self.commands_dict: Dict[str, BaseCommand] = {} 45 | 46 | # populating the dict 47 | for cmd in populate_commands(BaseCommand): 48 | command_name = cmd.__module__.split('.')[-1] 49 | self.commands_dict[command_name] = cmd() 50 | 51 | # add options to parser 52 | self.add_arguments(parser) 53 | 54 | def add_arguments(self, parser: ArgumentParser) -> None: 55 | 56 | sub_parser = parser.add_subparsers(dest='command') 57 | # add all the commands 58 | for name, subcommand in self.commands_dict.items(): 59 | subcommand_parser = sub_parser.add_parser(name, help=subcommand.get_desc()) 60 | # call add_options of the specific command class 61 | subcommand.add_arguments(subcommand_parser) 62 | 63 | def run_command(self, args: Namespace): 64 | # if command is valid, run the run_command function of the specific class 65 | if args.command: 66 | self.commands_dict[args.command].run_command(args) 67 | -------------------------------------------------------------------------------- /quickmail/commands/clear.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | from argparse import ArgumentParser, Namespace 5 | from zope.interface import implementer 6 | from quickmail.commands import ICommand 7 | from quickmail.utils.misc import quick_mail_dir, heavy_tick, quick_mail_template_dir 8 | 9 | 10 | @implementer(ICommand) 11 | class ClearCommand: 12 | def add_arguments(self, parser: ArgumentParser) -> None: 13 | parser.add_argument('-j', 14 | '--justdoit', 15 | action='store_true', 16 | help='clear storage including the credentials and token') 17 | parser.description = 'Use the clear command to clear all email body that are saved in your home directories. ' \ 18 | 'Additionally, pass --justdoit to remove the credential files as well' 19 | 20 | def run_command(self, args: Namespace): 21 | if not os.path.exists(quick_mail_dir): 22 | print('Storage already is empty ' + heavy_tick) 23 | return 24 | 25 | if args.justdoit: 26 | saved_files = [file for file in os.listdir(quick_mail_dir) if (file.endswith('.json') or file.endswith('.pickle'))] 27 | for file in saved_files: 28 | os.remove(quick_mail_dir + '/' + file) 29 | else: 30 | saved_files = [file for file in os.listdir(quick_mail_dir) if file.endswith('.txt')] 31 | for file in saved_files: 32 | os.remove(quick_mail_dir + '/' + file) 33 | 34 | saved_files = [file for file in os.listdir(quick_mail_template_dir) if file.endswith('.txt')] 35 | for file in saved_files: 36 | os.remove(quick_mail_template_dir + file) 37 | 38 | print('Storage cleared ' + heavy_tick + heavy_tick) 39 | 40 | def get_desc(self) -> str: 41 | return 'clear the body of message from local or even the token if --justdoit argument is added' 42 | -------------------------------------------------------------------------------- /quickmail/commands/init.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import pickle 4 | import os.path 5 | import shutil 6 | 7 | from google_auth_oauthlib.flow import InstalledAppFlow 8 | from google.auth.transport.requests import Request 9 | 10 | from argparse import ArgumentParser, Namespace 11 | from zope.interface import implementer 12 | from quickmail.commands import ICommand 13 | from quickmail.utils.misc import heavy_tick, heavy_exclamation, quick_mail_dir, quick_mail_creds_file, quick_mail_token_file 14 | 15 | from argparse import RawTextHelpFormatter 16 | 17 | SCOPES = ['https://www.googleapis.com/auth/gmail.send', 18 | 'https://www.googleapis.com/auth/gmail.readonly'] 19 | 20 | 21 | @implementer(ICommand) 22 | class InitCommand: 23 | def add_arguments(self, parser: ArgumentParser) -> None: 24 | parser.add_argument('filepath', 25 | help="Path to credentials.json") 26 | 27 | parser.formatter_class = RawTextHelpFormatter 28 | parser.description = f'''Use the init command to initialise the token and to set up your gmail account for hassle-free mail deliveries. 29 | 30 | If you don\'t have a credentials.json, head on to https://console.developers.google.com/apis/credentials/ and generate your credentials.json (select app type as Desktop App)''' 31 | 32 | def check_creds_json(self, path): 33 | 34 | if os.path.exists(quick_mail_dir): 35 | if os.path.exists(quick_mail_creds_file): 36 | print('Credentials file already exists, skipping... ' + heavy_tick) 37 | return 38 | else: 39 | print('Placed credentials file ' + heavy_tick) 40 | shutil.copy2(path, quick_mail_creds_file) 41 | else: 42 | os.makedirs(quick_mail_dir) 43 | shutil.copy2(path, quick_mail_creds_file) 44 | print('Saved credentials file ' + heavy_tick) 45 | 46 | def run_command(self, args: Namespace): 47 | 48 | self.check_creds_json(args.filepath) 49 | creds = None 50 | 51 | try: 52 | if os.path.exists(quick_mail_token_file): 53 | with open(quick_mail_token_file, 'rb') as token: 54 | creds = pickle.load(token) 55 | 56 | if not creds: 57 | flow = InstalledAppFlow.from_client_secrets_file( 58 | quick_mail_creds_file, SCOPES) 59 | creds = flow.run_local_server(port=0) 60 | 61 | with open(quick_mail_token_file, 'wb') as token: 62 | pickle.dump(creds, token) 63 | print('Generated token ' + heavy_tick) 64 | 65 | elif not creds.valid or creds.expired: 66 | creds.refresh(Request()) 67 | with open(quick_mail_token_file, 'wb') as token: 68 | pickle.dump(creds, token) 69 | print('Initialised token ' + heavy_tick) 70 | 71 | else: 72 | print('Initialised token ' + heavy_tick) 73 | 74 | except pickle.UnpicklingError: 75 | 76 | if os.path.exists(quick_mail_token_file): 77 | os.remove(quick_mail_token_file) 78 | 79 | print('Token file corrupted ' + heavy_exclamation + ' regenerating...') 80 | flow = InstalledAppFlow.from_client_secrets_file( 81 | quick_mail_creds_file, SCOPES) 82 | creds = flow.run_local_server(port=0) 83 | 84 | with open(quick_mail_token_file, 'wb') as token: 85 | pickle.dump(creds, token) 86 | 87 | print('Initialised token ' + heavy_tick) 88 | 89 | def get_desc(self) -> str: 90 | return 'initialise token and set your email id' 91 | -------------------------------------------------------------------------------- /quickmail/commands/send.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import mimetypes 4 | import os 5 | import pickle 6 | import base64 7 | from email.mime.application import MIMEApplication 8 | from email.mime.audio import MIMEAudio 9 | from email.mime.image import MIMEImage 10 | from email.mime.multipart import MIMEMultipart 11 | from email.mime.text import MIMEText 12 | from googleapiclient.discovery import build 13 | 14 | from argparse import ArgumentParser, Namespace 15 | from zope.interface import implementer 16 | from quickmail.commands import ICommand 17 | from quickmail.utils.misc import quick_mail_dir, quick_mail_token_file, grinning_face, heavy_tick, smiling_face, \ 18 | party_popper_tada, heavy_exclamation, quick_mail_template_dir 19 | from datetime import datetime 20 | import subprocess 21 | 22 | 23 | @implementer(ICommand) 24 | class SendCommand: 25 | def add_arguments(self, parser: ArgumentParser) -> None: 26 | parser.add_argument('-r', 27 | '--receiver', 28 | required=True, 29 | help="receiver's email address, eg. '-r \"xyz@gmail.com\"' ") 30 | parser.add_argument('-sub', 31 | '--subject', 32 | required=True, 33 | help="email's subject, eg. '-sub \"XYZ submission\"'") 34 | parser.add_argument('-t', 35 | '--template', 36 | help="template of email body, eg. '-t=\"assignment_template\"' ") 37 | parser.add_argument('-b', 38 | '--body', 39 | help="email's body, eg. '-b \"Message Body Comes Here\"'") 40 | parser.add_argument('-a', 41 | '--attachment', 42 | help="email's attachment path, eg. '~/Desktop/XYZ_Endsem.pdf' ") 43 | parser.add_argument('-l', 44 | '--lessgo', 45 | action='store_true', 46 | help='skip confirmation before sending mail') 47 | 48 | parser.description = 'Use the send command to send mail. Body can be passed as an argument, or typed in a ' \ 49 | 'nano shell. Use optional --lessgo command for sending mail without confirmation' 50 | 51 | def run_command(self, args: Namespace): 52 | 53 | creds = None 54 | 55 | # if token.pickle file is missing, init command should be run 56 | if os.path.exists(quick_mail_token_file): 57 | with open(quick_mail_token_file, 'rb') as token: 58 | creds = pickle.load(token) 59 | else: 60 | print('Could not find credentials, please run init command first ' + heavy_exclamation + heavy_exclamation) 61 | exit(0) 62 | 63 | receiver_email = args.receiver 64 | subject = args.subject 65 | body = args.body 66 | attachment = args.attachment 67 | template = args.template 68 | 69 | if not body and not template: 70 | body_file_name = '/' + datetime.now().strftime("%d_%m_%Y_%H_%M_%S") + '.txt' 71 | file_path = quick_mail_dir + body_file_name 72 | 73 | f = open(file_path, "x") 74 | # print(file_path) 75 | try: 76 | subprocess.call(['nano', file_path]) 77 | except OSError: 78 | f.close() 79 | print('Nano not found, please install nano or type the body inline') 80 | exit(0) 81 | 82 | f.close() 83 | f = open(file_path, "r") 84 | body = f.read() 85 | # print(body) 86 | print('\nAdded body of the mail ' + heavy_tick) 87 | f.close() 88 | 89 | elif not body: 90 | 91 | template_path = quick_mail_template_dir + template + '.txt' 92 | 93 | if not os.path.exists(template_path): 94 | print('Template doesn\'t exists, exiting...') 95 | exit(0) 96 | 97 | f = open(template_path, "r") 98 | template_txt = f.read() 99 | f.close() 100 | 101 | body_file_name = '/' + datetime.now().strftime("%d_%m_%Y_%H_%M_%S") + '.txt' 102 | file_path = quick_mail_dir + body_file_name 103 | 104 | f = open(file_path, "a") 105 | f.write(template_txt) 106 | f.close() 107 | 108 | try: 109 | subprocess.call(['nano', file_path]) 110 | except OSError: 111 | print('Nano not found, please install nano or type the body inline') 112 | exit(0) 113 | 114 | f = open(file_path, "r") 115 | body = f.read() 116 | # print(body) 117 | print('\nAdded body of the mail ' + heavy_tick) 118 | f.close() 119 | # Hacky fix to add a new line if body is not null 120 | else: 121 | print() 122 | 123 | print('Preparing to send mail ' + grinning_face) 124 | 125 | # build service 126 | service = build('gmail', 'v1', credentials=creds) 127 | 128 | # get user's email address from token file 129 | senders_email = service.users().getProfile(userId='me').execute()['emailAddress'] 130 | 131 | # Show user mail summary 132 | print('\nFrom: ' + senders_email + '\nTo: ' + receiver_email + '\nSubject: ' + subject + '\nBody\n' + body + 133 | '\nAttachment Path: ' + (str(attachment) if attachment else 'No attachments') + '\n') 134 | 135 | if not args.lessgo: 136 | is_confirm = str(input('Confirm send? (Y/N): ')) 137 | if is_confirm.lower() != 'y': 138 | print('Confirmation denied, exiting... ' + smiling_face + smiling_face + smiling_face) 139 | exit(0) 140 | 141 | if not attachment: 142 | message = MIMEText(body) 143 | 144 | message['to'] = receiver_email 145 | message['from'] = senders_email 146 | message['subject'] = subject 147 | 148 | else: 149 | message = MIMEMultipart() 150 | 151 | message['to'] = receiver_email 152 | message['from'] = senders_email 153 | message['subject'] = subject 154 | 155 | message.attach(MIMEText(body)) 156 | content_type, encoding = mimetypes.guess_type(attachment) 157 | 158 | if content_type is None or encoding is not None: 159 | content_type = 'application/octet-stream' 160 | 161 | main_type, sub_type = content_type.split('/', 1) 162 | 163 | if main_type == 'text': 164 | fp = open(attachment, 'rb') 165 | msg = MIMEText(str(fp.read().decode('utf-8')), _subtype=sub_type) 166 | fp.close() 167 | 168 | elif main_type == 'image': 169 | fp = open(attachment, 'rb') 170 | msg = MIMEImage(fp.read(), _subtype=sub_type) 171 | fp.close() 172 | 173 | elif main_type == 'audio': 174 | fp = open(attachment, 'rb') 175 | msg = MIMEAudio(fp.read(), _subtype=sub_type) 176 | fp.close() 177 | 178 | else: 179 | fp = open(attachment, 'rb') 180 | msg = MIMEApplication(fp.read(), _subtype=sub_type) 181 | fp.close() 182 | 183 | filename = os.path.basename(attachment) 184 | msg.add_header('Content-Disposition', 'attachment', filename=filename) 185 | message.attach(msg) 186 | 187 | raw = base64.urlsafe_b64encode(message.as_bytes()) 188 | raw = raw.decode() 189 | message = {'raw': raw} 190 | 191 | try: 192 | message = (service.users().messages().send(userId=senders_email, body=message) 193 | .execute()) 194 | print('Mail delivered ' + party_popper_tada + party_popper_tada + party_popper_tada) 195 | except BaseException as e: 196 | print('Could not send message: ', e) 197 | 198 | print() 199 | 200 | def get_desc(self) -> str: 201 | return 'send the mail' 202 | -------------------------------------------------------------------------------- /quickmail/commands/template.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import subprocess 5 | from argparse import ArgumentParser, Namespace 6 | from zope.interface import implementer 7 | from quickmail.commands import ICommand 8 | from quickmail.utils.misc import heavy_tick, quick_mail_template_dir, party_popper_tada 9 | 10 | 11 | @implementer(ICommand) 12 | class ClearCommand: 13 | 14 | def add_arguments(self, parser: ArgumentParser) -> None: 15 | 16 | subp = parser.add_subparsers(dest='template_subcommand') 17 | 18 | subp.add_parser('add', help='add a new template') \ 19 | .add_argument('-n', 20 | '--templatename', 21 | required=True, 22 | help='name of the new template') 23 | 24 | subp.add_parser('listall', help='list all templates') 25 | 26 | subp.add_parser('edit', help='edit a particular template') \ 27 | .add_argument('-n', 28 | '--templatename', 29 | required=True, 30 | help='name of the new template') 31 | 32 | parser.description = 'manage mail templates' 33 | 34 | def run_command(self, args: Namespace): 35 | 36 | if args.template_subcommand == 'add': 37 | if not os.path.exists(quick_mail_template_dir): 38 | os.makedirs(quick_mail_template_dir) 39 | 40 | file_path = quick_mail_template_dir + args.templatename + '.txt' 41 | 42 | f = open(file_path, "x") 43 | # print(file_path) 44 | try: 45 | subprocess.call(['nano', file_path]) 46 | f.close() 47 | except OSError: 48 | f.close() 49 | print('Nano not found, please install nano before you can proceed.') 50 | exit(0) 51 | 52 | print('Template created, at ' + file_path + ' ' + party_popper_tada + party_popper_tada) 53 | 54 | elif args.template_subcommand == 'listall': 55 | templates = [file for file in os.listdir(quick_mail_template_dir) if file.endswith('.txt')] 56 | for template in reversed(templates): 57 | # remove '.txt' from template name 58 | template = template[:-4] 59 | print(template) 60 | elif args.template_subcommand == 'edit': 61 | 62 | file_path = quick_mail_template_dir + args.templatename + '.txt' 63 | 64 | if not os.path.exists(file_path): 65 | print('Template doesn\'t exists, created new one at ' + file_path + ' ' + heavy_tick) 66 | f = open(file_path, "x") 67 | f.close() 68 | 69 | f = open(file_path, "a") 70 | 71 | try: 72 | subprocess.call(['nano', file_path]) 73 | f.close() 74 | except OSError: 75 | f.close() 76 | print('Nano not found, please install nano before you can proceed.') 77 | exit(0) 78 | 79 | print('Template edited, check: ' + file_path + ' ' + party_popper_tada + party_popper_tada) 80 | 81 | def get_desc(self) -> str: 82 | return 'manage templates of mail body' 83 | -------------------------------------------------------------------------------- /quickmail/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avikumar15/quick-mail-cli/4282fb62794671de89d5d8efc3697b0a0d540230/quickmail/utils/__init__.py -------------------------------------------------------------------------------- /quickmail/utils/misc.py: -------------------------------------------------------------------------------- 1 | import os 2 | from importlib import import_module 3 | from pkgutil import iter_modules 4 | 5 | quick_mail_dir = os.path.expanduser('~/.quickmail') 6 | quick_mail_creds_file = os.path.expanduser('~/.quickmail/credentials.json') 7 | quick_mail_token_file = os.path.expanduser('~/.quickmail/token.pickle') 8 | quick_mail_template_dir = os.path.expanduser('~/.quickmail/templates/') 9 | command_dir_path = 'quickmail.commands' 10 | 11 | # Emojis 12 | heavy_tick = '\u2705' 13 | heavy_exclamation = '\u2757' 14 | wink_face = '\U0001F609' 15 | grinning_face = '\U0001F601' 16 | thinking_cloud = '\U0001F4AC' 17 | party_popper_tada = '\U0001F389' 18 | sasta_tada = '\U0001F38A' 19 | military_medal = '\U0001F396' 20 | trophy = '\U0001F3C6' 21 | first_medal = '\U0001F947' 22 | smiling_face = '\U0001F642' 23 | 24 | 25 | def walk_modules(path): 26 | """Loads a module and all its submodules from the given module path and 27 | returns them. If *any* module throws an exception while importing, that 28 | exception is thrown back. 29 | """ 30 | 31 | mods = [] 32 | mod = import_module(path) 33 | mods.append(mod) 34 | if hasattr(mod, '__path__'): 35 | for _, sub_path, is_package in iter_modules(mod.__path__): 36 | full_path = path + '.' + sub_path 37 | if is_package: 38 | mods += walk_modules(full_path) 39 | else: 40 | sub_module = import_module(full_path) 41 | mods.append(sub_module) 42 | return mods 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | AccessControl>=5.0 2 | Acquisition>=4.7 3 | AuthEncoding>=4.2 4 | beautifulsoup4>=4.9.3 5 | BTrees>=4.7.2 6 | cachetools>=4.1.1 7 | certifi>=2020.11.8 8 | cffi>=1.14.4 9 | Chameleon>=3.8.1 10 | chardet==3.0.4 11 | DateTime>=4.3 12 | DocumentTemplate>=4.0 13 | ExtensionClass>=4.5.0 14 | google-api-core>=1.25.0 15 | google-api-python-client>=1.12.8 16 | google-auth>=1.23.0 17 | google-auth-httplib2>=0.0.4 18 | google-auth-oauthlib>=0.4.2 19 | googleapis-common-protos>=1.52.0 20 | httplib2>=0.19.0 21 | idna>=2.10 22 | MultiMapping>=4.1 23 | oauthlib>=3.1.0 24 | PasteDeploy>=2.1.1 25 | Persistence>=3.0 26 | persistent>=4.6.4 27 | protobuf>=3.14.0 28 | pyasn1>=0.4.8 29 | pyasn1-modules>=0.2.8 30 | pycparser>=2.20 31 | python-gettext>=4.0 32 | pytz>=2020.4 33 | requests>=2.25.0 34 | requests-oauthlib>=1.3.0 35 | RestrictedPython>=5.1 36 | roman>=3.3 37 | rsa>=4.6 38 | six>=1.15.0 39 | soupsieve>=2.0.1 40 | transaction>=3.0.0 41 | uritemplate>=3.0.1 42 | urllib3>=1.26.4 43 | waitress>=1.4.4 44 | WebOb>=1.8.6 45 | WebTest>=2.0.35 46 | WSGIProxy2>=0.4.6 47 | z3c.pt>=3.3.0 48 | zc.lockfile>=2.0 49 | ZConfig>=3.5.0 50 | zExceptions>=4.1 51 | ZODB>=5.6.0 52 | zodbpickle>=2.0.0 53 | zope.annotation>=4.7.0 54 | zope.browser>=2.3 55 | zope.browsermenu>=4.4 56 | zope.browserpage>=4.4.0 57 | zope.browserresource>=4.4 58 | zope.cachedescriptors>=4.3.1 59 | zope.component>=4.6.2 60 | zope.configuration>=4.4.0 61 | zope.container>=4.4.0 62 | zope.contentprovider>=4.2.1 63 | zope.contenttype>=4.5.0 64 | zope.deferredimport>=4.3.1 65 | zope.deprecation>=4.4.0 66 | zope.dottedname>=4.3 67 | zope.event>=4.5.0 68 | zope.exceptions>=4.4 69 | zope.filerepresentation>=5.0.0 70 | zope.globalrequest>=1.5 71 | zope.hookable>=5.0.1 72 | zope.i18n>=4.7.0 73 | zope.i18nmessageid>=5.0.1 74 | zope.interface>=5.2.0 75 | zope.lifecycleevent>=4.3 76 | zope.location>=4.2 77 | zope.pagetemplate>=4.5.0 78 | zope.processlifetime>=2.3.0 79 | zope.proxy>=4.3.5 80 | zope.ptresource>=4.2.0 81 | zope.publisher>=5.2.1 82 | zope.schema>=6.0.0 83 | zope.security>=5.1.1 84 | zope.sequencesort>=4.1.2 85 | zope.site>=4.4.0 86 | zope.size>=4.3 87 | zope.structuredtext>=4.3 88 | zope.tal>=4.4 89 | zope.tales>=5.1 90 | zope.testbrowser>=5.5.1 91 | zope.testing>=4.7 92 | zope.traversing>=4.4.1 93 | zope.viewlet>=4.2.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | from setuptools import setup, find_packages 5 | 6 | current_path = pathlib.Path(__file__).parent 7 | 8 | with open(os.path.join(current_path, 'requirements.txt'), encoding='utf-8') as f: 9 | install_requires = f.read().split('\n') 10 | 11 | """ 12 | print(install_requires) 13 | print(open('PYPI_DESCRIPTION.md', 'r', encoding='utf-8').read()) 14 | """ 15 | setup( 16 | name='quick-mail', 17 | description='A simple commandline application for sending mails quickly', 18 | version='1.0.4', 19 | install_requires=install_requires, 20 | author='Avi Kumar Singh', 21 | author_email='avikumar.singh1508@gmail.com', 22 | python_requires='>=3.0', 23 | long_description_content_type="text/markdown", 24 | long_description=open('PYPI_DESCRIPTION.md', 'r', encoding='utf-8').read(), 25 | packages=find_packages(), 26 | # package_dir={'': 'quickmail'}, 27 | entry_points={'console_scripts': ['quickmail=quickmail.cli:execute']}, 28 | url='https://github.com/avikumar15/quick-email-cli', 29 | license="MIT", 30 | keywords=['CLI', 'gmail', 'email'], 31 | # platforms='any', 32 | # http://pypi.python.org/pypi?%3Aaction=list_classifiers 33 | classifiers=[ 34 | "Development Status :: 3 - Alpha", 35 | "Environment :: Console", 36 | 'Topic :: Education', 37 | 'Topic :: Communications', 38 | 'Topic :: Communications :: Email', 39 | 'Topic :: Internet', 40 | 'Topic :: Terminals', 41 | 'License :: OSI Approved :: MIT License', 42 | 'Intended Audience :: Education', 43 | 'Intended Audience :: Other Audience', 44 | 'Operating System :: Unix', 45 | 'Operating System :: POSIX', 46 | 'Operating System :: MacOS', 47 | 'Operating System :: MacOS :: MacOS X', 48 | 'Operating System :: Microsoft', 49 | 'Operating System :: Microsoft :: Windows', 50 | 'Operating System :: Microsoft :: Windows :: Windows 10', 51 | 'Operating System :: OS Independent', 52 | 'Operating System :: POSIX :: Linux', 53 | 'Programming Language :: Python', 54 | 'Programming Language :: Python :: 3', 55 | ], 56 | ) 57 | --------------------------------------------------------------------------------