diff --git a/.gitignore b/.gitignore index b6e4761..626564d 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,10 @@ ipython_config.py # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ +#extra files +test.py + + # Celery stuff celerybeat-schedule celerybeat.pid diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3b66410 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5548df9..41af626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,5 +7,7 @@ ## 0.0.5 - Added one more config variable named `MAIL_DEFAULT_SENDER`. It's as same as `MAIL_FROM` config var. - Fixed absent of `httpx` module at the __setup.py__ file. +- Config var `MAIL_SSL` and `MAIL_TLS` changes to `MAIL_USE_SSL` and `MAIL_USE_TLS` accordingly. +- added `add_recipient` and `attach` method to the __schemas.Message__ class. - Fixed some broken test cases. - modifications at the documentation. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index e69de29..26e84f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-exclude flask-mailing/examples * \ No newline at end of file diff --git a/README.md b/README.md index 9a64d51..802768b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # Flask-Mailing +![Flask mail logo](https://github.com/marktennyson/flask-mailing/blob/main/flask-mailing-logo-cropped.png?raw=true) + Flask-Mailing adds SMTP mail sending to your Flask applications Flask_Mail is dead now. To use the mail service with your project you can use eaither [Flask-Mailing](https://github.com/marktennyson/flask-mailing) for legacy or [Flask-Mailman](https://github.com/waynerv/flask-mailman) for Django type implementation. Flask-Mailing is a fork of `Sabuhi's` Fastapi-Mail package, providing similar functionality. 99% of the work was done by him, and the fork was made mainly provide the same features and the apis for the Flask Microframework. -##### Need help to create and deploy the test cases.(Urgent) +# Downloads +[![Downloads](https://pepy.tech/badge/flask-mailing)](https://pepy.tech/project/flask-mailing) [![Downloads](https://pepy.tech/badge/flask-mailing/month)](https://pepy.tech/project/flask-mailing) [![Downloads](https://pepy.tech/badge/flask-mailing/week)](https://pepy.tech/project/flask-mailing) +
+ ### 🔨 Installation ### @@ -48,8 +53,8 @@ app.config['MAIL_USERNAME'] = "YourUserName" app.config['MAIL_PASSWORD'] = "strong_password" app.config['MAIL_PORT'] = 587 app.config['MAIL_SERVER'] = "your mail server" -app.config['MAIL_TLS'] = True -app.config['MAIL_SSL'] = False +app.config['MAIL_USE_TLS'] = True +app.config['MAIL_USE_SSL'] = False app.config['USE_CREDENTIALS'] = True app.config['VALIDATE_CERTS'] = True app.config['MAIL_DEFAULT_SENDER'] = "youremailid@doaminname.com" diff --git a/docs/example.md b/docs/example.md index 6de0e2a..8be4144 100644 --- a/docs/example.md +++ b/docs/example.md @@ -21,8 +21,8 @@ def create_app(): app.config['MAIL_PASSWORD'] = "world_top_secret_password" app.config['MAIL_PORT'] = 587 app.config['MAIL_SERVER'] = "your-email-server.com" - app.config['MAIL_TLS'] = True - app.config['MAIL_SSL'] = False + app.config['MAIL_USE_TLS'] = True + app.config['MAIL_USE_SSL'] = False mail.init_app(app) return app @@ -45,6 +45,13 @@ async def simple_send(): return jsonify(status_code=200, content={"message": "email has been sent"}) ``` +#### Add recipient using `add_recipient` method + +```python +message.add_recipient("recipient@emldomain.com") +``` + + ### Send a simple html message ```python @@ -81,6 +88,11 @@ async def mail_file(): await mail.send_message(message) return jsonify(message="email sent") ``` +#### Sending files using `attach` method +```python +with app.open_resource("attachments/example.txt") as fp: + message.attach("example.txt", fp.read()) +``` ### Using Jinja2 HTML Templates diff --git a/docs/getting-started.md b/docs/getting-started.md index f880810..dfa0364 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -12,8 +12,8 @@ class has following attributes - If you service does not provide username use sender address for connection. - **MAIL_PASSWORD** : Password for authentication - **MAIL_SERVER** : SMTP Mail server. -- **MAIL_TLS** : For TLS connection -- **MAIL_SSL** : For TLS connection +- **MAIL_USE_TLS** : For TLS connection +- **MAIL_USE_SSL** : For TLS connection - **MAIL_DEBUG** : Debug mode for while sending mails, defaults 0. - **MAIL_FROM** : Sender address - **MAIL_DEFAULT_SENDER** : Sender address @@ -44,10 +44,10 @@ class has following attributes - reply_to : Reply-To recipients in the mail - charset : charset defaults to utf-8 - subtype : subtype of the mail defaults to plain +- add_recipient : a method to add additional recipients. +- attach : a method to add additional attachments. - - ### ```utils.DefaultChecker``` class Default class for checking email from collected public resource. The class makes it possible to use redis to save data. diff --git a/docs/index.md b/docs/index.md index 1d6b820..aa1d664 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,8 +29,8 @@ def create_app(): app.config['MAIL_PASSWORD'] = "world_top_secret_password" app.config['MAIL_PORT'] = 587 app.config['MAIL_SERVER'] = "your-email-server.com" - app.config['MAIL_TLS'] = True - app.config['MAIL_SSL'] = False + app.config['MAIL_USE_TLS'] = True + app.config['MAIL_USE_SSL'] = False app.config['MAIL_DEFAULT_SENDER'] = "your-email@your-domain.com" mail.init_app(app) diff --git a/examples/base.py b/examples/base.py index 704112f..63829dc 100644 --- a/examples/base.py +++ b/examples/base.py @@ -1,16 +1,18 @@ from flask import Flask +import os as os + def create_app(): app = Flask(__name__) - app.config['MAIL_USERNAME'] = "aniketsarkar1998@gmail.com" - app.config['MAIL_PASSWORD'] = "" + app.config['MAIL_USERNAME'] = os.environ['mu'] + app.config['MAIL_PASSWORD'] = os.environ['mp'] app.config['MAIL_FROM'] = "aniketsarkar1998@gmail.com" - app.config['MAIL_PORT'] = 587 - app.config['MAIL_SERVER'] = "smtp.gmail.com" - app.config['MAIL_TLS'] = True - app.config['MAIL_SSL'] = False + app.config['MAIL_PORT'] = os.environ['mport'] + app.config['MAIL_SERVER'] = os.environ['ms'] + app.config['MAIL_USE_TLS'] = True + app.config['MAIL_USE_SSL'] = False # app.config['MAIL_TEMPLATE_FOLDER'] = Path(__file__).parent / 'attachments' return app \ No newline at end of file diff --git a/examples/application.py b/examples/main.py similarity index 78% rename from examples/application.py rename to examples/main.py index a1ec335..1b5b4ab 100644 --- a/examples/application.py +++ b/examples/main.py @@ -1,7 +1,11 @@ +from dotenv import load_dotenv; load_dotenv() + from flask import jsonify from base import create_app from flask_mailing import Mail, Message +import os as os + mail = Mail() @@ -24,10 +28,10 @@ async def simple_send() -> jsonify: message = Message( subject="Flask-Mailing module", - recipients=["aniketsarkar@yahoo.com"], + recipients=[os.environ['MAIL_RECIPIENT']], body="This is the basic email body", - # subtype="html" ) + message.add_recipient("aniforsana@gmail.com") await mail.send_message(message) @@ -38,10 +42,13 @@ async def simple_send() -> jsonify: async def mail_file(): message = Message( subject = "attachments based email", - recipients = ["aniketsarkar@yahoo.com"], + recipients = [os.environ['MAIL_RECIPIENT']], body = "This is the email body", attachments = ['attachments/attachment.txt'] ) + with app.open_resource('attachments/test.html') as fp: + message.attach("test.html", fp.read()) + await mail.send_message(message) return jsonify(message="email sent") @@ -50,7 +57,7 @@ async def mail_html(): message = Message( subject = "html template based email", - recipients = ["aniketsarkar@yahoo.com"], + recipients = [os.environ['MAIL_RECIPIENT']], template_body = { "first_name": "Fred", "last_name": "Fredsson" diff --git a/flask_mailing/mail.py b/flask_mailing/mail.py index 51c40ad..49e7db8 100644 --- a/flask_mailing/mail.py +++ b/flask_mailing/mail.py @@ -50,10 +50,7 @@ class Mail(_MailMixin): """ - def __init__(self, - app:t.Optional["Flask"]=None - # config: ConnectionConfig - ): + def __init__(self, app:t.Optional["Flask"]=None) -> None: if app is not None: self.init_app(app) @@ -65,8 +62,8 @@ def init_app(self, app:"Flask") -> None: MAIL_PASSWORD = app.config.get("MAIL_PASSWORD"), MAIL_PORT = app.config.get("MAIL_PORT", 465), MAIL_SERVER = app.config.get("MAIL_SERVER"), - MAIL_TLS = app.config.get("MAIL_TLS", False), - MAIL_SSL = app.config.get("MAIL_SSL", True), + MAIL_TLS = app.config.get("MAIL_USE_TLS", False), + MAIL_SSL = app.config.get("MAIL_USE_SSL", True), MAIL_DEBUG= app.config.get("MAIL_DEBUG", 0), MAIL_FROM_NAME = app.config.get("MAIL_FROM_NAME",None), MAIL_TEMPLATE_FOLDER = app.config.get("MAIL_TEMPLATE_FOLDER",None), diff --git a/flask_mailing/msg.py b/flask_mailing/msg.py index afecbab..169ffdf 100644 --- a/flask_mailing/msg.py +++ b/flask_mailing/msg.py @@ -7,6 +7,11 @@ from email.utils import formatdate, make_msgid from email.encoders import encode_base64 +import typing as t + +if t.TYPE_CHECKING: + from werkzeug.datastructures import FileStorage + PY3 = sys.version_info[0] == 3 @@ -40,8 +45,7 @@ def _mimetext(self, text, subtype="plain"): return MIMEText(text, _subtype=subtype, _charset=self.charset) - async def attach_file(self, message, attachment): - + async def attach_file(self, message, attachment:t.List["FileStorage"]): for file in attachment: part = MIMEBase(_maintype="application", _subtype="octet-stream") @@ -58,14 +62,16 @@ async def attach_file(self, message, attachment): filename = filename.encode('utf8') filename = ('UTF8', '', filename) - + disposition:str = getattr(file, 'disposition', 'attachment') part.add_header( 'Content-Disposition', - "attachment", + disposition, filename=filename) self.message.attach(part) + file.close() # close the file stream + async def _message(self, sender): """Creates the email message""" diff --git a/flask_mailing/schemas.py b/flask_mailing/schemas.py index e0e1b55..703611e 100644 --- a/flask_mailing/schemas.py +++ b/flask_mailing/schemas.py @@ -1,6 +1,14 @@ import os +import io from enum import Enum -from typing import List, Union, Any, Optional, Dict +from typing import ( + List, + Union, + Any, + Optional, + Dict, + Literal + ) from mimetypes import MimeTypes from pydantic import BaseModel, EmailStr, validator @@ -68,6 +76,48 @@ def validate_subtype(cls, value, values, config, field): return 'html' return value + + def add_recipient(self, recipient:str) -> Literal[True]: + """ + Adds another recipient to the message. + + :param recipient: email address of recipient. + """ + self.recipients.append(recipient) + return True + + + def attach( + self, + filename:str, + data:Union[bytes,str], + content_type:dict=None, + disposition:str='attachment', + headers:dict={} + ) -> Literal[True]: + """ + Adds an attachment to the message. + + :param `filename`: filename of attachment + :param `data`: the raw file data + :param `content_type`: file mimetype + :param `disposition`: content-disposition (if any) + :param `headers`: dictionary of headers + """ + if content_type is None: + mime = MimeTypes() + content_type = mime.guess_type(filename) + + fsob:"FileStorage" = FileStorage( + io.BytesIO(data), + filename, + content_type=content_type, + headers=headers + ) + fsob.disposition = disposition + self.attachments.append(fsob) + return True + def validate_path(path): cur_dir = os.path.abspath(os.curdir) diff --git a/logo/flask-mailing-logo-cropped.png b/logo/flask-mailing-logo-cropped.png new file mode 100644 index 0000000..cb21954 Binary files /dev/null and b/logo/flask-mailing-logo-cropped.png differ diff --git a/logo/flask-mailing-logo-full.png b/logo/flask-mailing-logo-full.png new file mode 100644 index 0000000..4e19100 Binary files /dev/null and b/logo/flask-mailing-logo-full.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 8c4bab6..5b81f5b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Flask-MAILING -site_url: +site_url: https://marktennyson.github.io/flask-mailing site_description: flask-mailing dev_addr: 127.0.0.1:8080 @@ -15,9 +15,9 @@ theme: -repo_name: flask-mailing +repo_name: marktennyson/flask-mailing repo_url: https://github.com/marktennyson/flask-mailing -edit_uri: '' +edit_uri: 'https://github.com/marktennyson/flask-mailing' plugins: - search diff --git a/setup.py b/setup.py index 6b8eedb..bc3c6d1 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,11 @@ -from setuptools import setup,find_packages +from setuptools import ( + setup, + find_packages + ) -version = (0, 0, 5) -author = "Aniket Sarkar" +VERSION = (0, 0, 5) +AUTHOR = "Aniket Sarkar" +AUTHOR_EMAIL = "aniketsarkar@yahoo.com" with open("README.md", "r") as f: @@ -10,11 +14,11 @@ setup( name="Flask-Mailing", - version=".".join([str(i) for i in list(version)]), + version=".".join([str(i) for i in list(VERSION)]), url="https://github.com/marktennyson/flask-mailing", license="MIT", - author=author, - author_email="aniketsarkar@yahoo.com", + author=AUTHOR, + author_email=AUTHOR_EMAIL, description="Flask mail system sending mails(individual, bulk) attachments(individual, bulk) fully asynchroniously", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/conftest.py b/tests/conftest.py index af6f33f..0a6df5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from pathlib import Path import pytest -# import fakeredis.aioredis +import fakeredis.aioredis from flask_mailing.utils import DefaultChecker @@ -12,15 +12,15 @@ def default_checker(): del test -# @pytest.fixture -# @pytest.mark.asyncio -# async def redis_checker(scope="redis_config"): -# test = DefaultChecker(db_provider="redis") -# test.redis_client = await fakeredis.aioredis.create_redis_pool(encoding="UTF-8") -# await test.init_redis() -# yield test -# await test.redis_client.flushall() -# await test.close_connections() +@pytest.fixture +@pytest.mark.asyncio +async def redis_checker(scope="redis_config"): + test = DefaultChecker(db_provider="redis") + test.redis_client = await fakeredis.aioredis.create_redis_pool(encoding="UTF-8") + await test.init_redis() + yield test + await test.redis_client.flushall() + await test.close_connections() @pytest.fixture(autouse=True) @@ -34,8 +34,8 @@ def mail_config(): "MAIL_FROM_NAME": "example", "MAIL_PORT": 25, "MAIL_SERVER": "localhost", - "MAIL_TLS": False, - "MAIL_SSL": False, + "MAIL_USE_TLS": False, + "MAIL_USE_SSL": False, "MAIL_DEBUG": 0, "SUPPRESS_SEND": 1, "USE_CREDENTIALS": False, diff --git a/tests/test_message.py b/tests/test_message.py index 9e8692d..961b41a 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -27,6 +27,17 @@ def test_recipients_properly_initialized(): assert message.recipients == [] +def test_add_recipient_method(): + message = Message( + subject="test subject", + recipients=[], + body="test", + subtype="plain" + ) + message.add_recipient("aniketsarkar@yahoo.com") + + assert message.recipients == ["aniketsarkar@yahoo.com"] + def test_sendto_properly_set(): msg = Message(subject="subject", recipients=["somebody@here.com", "somebody2@here.com"], @@ -88,6 +99,21 @@ def test_plain_message_with_attachments(): assert len(msg.attachments) == 1 +def test_plain_message_with_attach_method(): + directory = os.getcwd() + attachement = directory + "/files/attachement_1.txt" + + msg = Message(subject="testing", + recipients=["to@example.com"], + body="test mail body") + + with open(attachement, "w") as file: + file.write(CONTENT) + + with open(attachement, "rb") as fp: + msg.attach("attachement_1.txt", fp.read()) + + assert len(msg.attachments) == 1 def test_empty_subject_header():