├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── send.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | ATTACH 2 | compose.md 3 | data.csv 4 | .env 5 | compose.html 6 | 7 | secrets.yml 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | token.txt 139 | venv 140 | .vscode 141 | t.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | https://opensource.org/licenses/MIT 3 | 4 | 2020 AAHNIK DAW 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bulk-email-sender 2 | 3 | 4 | Send Templatized Dynamic Emails Automatically 5 | 6 | ## What's New 7 | 8 | - You can send html mails. Just write `compose.html`. 9 | 10 | ## Features 11 | 12 | This is a simple program which does its work perfectly. Nothing more, nothing less 13 | 14 | 1. Send dynamic emails with unlimited variables pulling data from a CSV file. 15 | 2. Supports Markdown Formatting & embed links or images. 16 | 3. Supports Attaching any kind of files. 17 | 18 | ## Usage 19 | 20 | - Make sure you have Python installed in your system. 21 | - Download or Clone the repo and then move into the `automailer` directory. 22 | - Install all dependancies: 23 | ```shell 24 | pip install -r requirements.txt 25 | ``` 26 | - Write your email inside **`compose.md`** (supports markdown formatting) 27 | - You can use **variables** , prefix them with `$` sign. 28 | 29 | > Hi $NAME , you have Bill Rs. $price due for $months 30 | 31 | - Put your data inside `data.csv` file 32 | *The line 1 ie headers must contain 'EMAIL' (uppercase) parameter* 33 | 34 | 35 | ![csv_image](https://user-images.githubusercontent.com/66209958/103172846-715d0c00-487c-11eb-9419-9dceb4297e49.png) 36 | 37 | *You can Export CSV file from Microsoft Office Excel, Libre Office, Google Sheets, SQL Database, or NoSQL Database* 38 | 39 | - You you want to put any attachments , put them in the `ATTACH` directory. 40 | - Create a file `.env` and put the following into it: 41 | 42 | ```text 43 | display_name=Mr.Bean 44 | sender_email=your@example.com 45 | password=12345 46 | ``` 47 | Make sure to put real values, the above values are just an example. 48 | - **Do not put original email password.** 49 | Create Gmail Account then turn on 2 step Verification, and then set up an [App Password](https://support.google.com/accounts/answer/185833?hl=en) for `automailer`. 50 | - All set up 👍 you are now READY TO GO . Run the `send.py` file: 51 | ```shell 52 | python3 send.py 53 | ``` 54 | - You will be asked to confirm the attachments in the `ATTACH` folder. Upon confirmation , the application will start sending emails. 55 | - You will receive a full success report after emails are sent. 56 | 57 | ## Getting Help 58 | 59 | Please report an issue or ask your question in the issues section of the repository. 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv 2 | Markdown -------------------------------------------------------------------------------- /send.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from settings import SENDER_EMAIL, PASSWORD, DISPLAY_NAME, MAIL_COMPOSE, SUBJECT 4 | 5 | from smtplib import SMTP 6 | import smtplib 7 | import markdown 8 | from email.mime.text import MIMEText 9 | from email.mime.multipart import MIMEMultipart 10 | from email.mime.base import MIMEBase 11 | from email import encoders 12 | import ssl 13 | 14 | 15 | def get_msg(csv_file_path, template): 16 | with open(csv_file_path, "r") as file: 17 | headers = file.readline().split(",") 18 | headers[len(headers) - 1] = headers[len(headers) - 1][:-1] 19 | # i am opening the csv file two times above and below INTENTIONALLY, changing will cause error 20 | with open(csv_file_path, "r") as file: 21 | data = csv.DictReader(file) 22 | for row in data: 23 | required_string = template 24 | for header in headers: 25 | value = row[header] 26 | required_string = required_string.replace(f"${header}", value) 27 | yield row["EMAIL"], required_string 28 | 29 | 30 | def confirm_attachments(): 31 | file_contents = [] 32 | file_names = [] 33 | try: 34 | for filename in os.listdir("ATTACH"): 35 | 36 | entry = input( 37 | f"""TYPE IN 'Y' AND PRESS ENTER IF YOU CONFIRM T0 ATTACH {filename} 38 | TO SKIP PRESS ENTER: """ 39 | ) 40 | confirmed = True if entry == "Y" else False 41 | if confirmed: 42 | file_names.append(filename) 43 | with open(f"{os.getcwd()}/ATTACH/{filename}", "rb") as f: 44 | content = f.read() 45 | file_contents.append(content) 46 | 47 | return {"names": file_names, "contents": file_contents} 48 | except FileNotFoundError: 49 | print("No ATTACH directory found...") 50 | 51 | 52 | def send_emails(server: SMTP, template, is_html): 53 | 54 | attachments = confirm_attachments() 55 | sent_count = 0 56 | 57 | for receiver, message in get_msg("data.csv", template): 58 | 59 | multipart_msg = MIMEMultipart("alternative") 60 | 61 | if SUBJECT: 62 | multipart_msg["Subject"] = SUBJECT 63 | else: 64 | multipart_msg["Subject"] = message.splitlines()[0] 65 | multipart_msg["From"] = DISPLAY_NAME + f" <{SENDER_EMAIL}>" 66 | multipart_msg["To"] = receiver 67 | 68 | text = message 69 | if not is_html: 70 | html = markdown.markdown(text) 71 | else: 72 | html = text 73 | 74 | part1 = MIMEText(text, "plain") 75 | part2 = MIMEText(html, "html") 76 | 77 | multipart_msg.attach(part1) 78 | multipart_msg.attach(part2) 79 | 80 | if attachments: 81 | for content, name in zip(attachments["contents"], attachments["names"]): 82 | attach_part = MIMEBase("application", "octet-stream") 83 | attach_part.set_payload(content) 84 | encoders.encode_base64(attach_part) 85 | attach_part.add_header( 86 | "Content-Disposition", f"attachment; filename={name}" 87 | ) 88 | multipart_msg.attach(attach_part) 89 | 90 | try: 91 | server.sendmail(SENDER_EMAIL, receiver, multipart_msg.as_string()) 92 | except Exception as err: 93 | print(f"Problem occurend while sending to {receiver} ") 94 | print(err) 95 | input("PRESS ENTER TO CONTINUE") 96 | else: 97 | sent_count += 1 98 | 99 | print(f"Sent {sent_count} emails") 100 | 101 | 102 | if __name__ == "__main__": 103 | host = "smtppro.zoho.in" 104 | port = 587 # TLS replaced SSL in 1999 105 | 106 | is_html = MAIL_COMPOSE.endswith("html") 107 | 108 | with open(MAIL_COMPOSE, "r", encoding="utf-8") as f: 109 | template = f.read() 110 | 111 | context = ssl.create_default_context() 112 | 113 | server = SMTP(host=host, port=port) 114 | server.connect(host=host, port=port) 115 | server.ehlo() 116 | # server.starttls(context=context) 117 | server.starttls() 118 | server.ehlo() 119 | server.login(user=SENDER_EMAIL, password=PASSWORD) 120 | print(SENDER_EMAIL, PASSWORD) 121 | 122 | # with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server: 123 | # server.login(SENDER_EMAIL, PASSWORD) 124 | send_emails(server, template, is_html) 125 | 126 | 127 | # AAHNIK 2023 128 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | DISPLAY_NAME = os.getenv("display_name") 8 | SENDER_EMAIL = os.getenv("sender_email") 9 | PASSWORD = os.getenv("password") 10 | MAIL_COMPOSE: str = os.getenv("mail_compose", "compose.md") 11 | SUBJECT: str | None = os.getenv("subject", None) 12 | 13 | try: 14 | assert DISPLAY_NAME 15 | assert SENDER_EMAIL 16 | assert PASSWORD 17 | assert MAIL_COMPOSE 18 | except AssertionError: 19 | print("Please set up credentials. Read https://github.com/aahnik/automailer#usage") 20 | else: 21 | print("Credentials loaded successfully") 22 | 23 | print(DISPLAY_NAME) 24 | print(SENDER_EMAIL) 25 | print(PASSWORD) 26 | print(SUBJECT) 27 | --------------------------------------------------------------------------------