├── .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 | [](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 | [](https://www.python.org/)
5 |
6 | [](https://pypi.org/project/quick-mail/)
7 | [](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 | [](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 |
--------------------------------------------------------------------------------