diff --git a/.github/workflows/markdown-validation.yml b/.github/workflows/markdown-validation.yml index 1d80805..c1c29ae 100644 --- a/.github/workflows/markdown-validation.yml +++ b/.github/workflows/markdown-validation.yml @@ -8,8 +8,6 @@ on: jobs: none-shall-pass: - runs-on: - - self-hosted - - Ubuntu + runs-on: thevickypedia-default steps: - uses: thevickypedia/none-shall-pass@v5 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 360a055..d9d4a8b 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,32 +1,14 @@ -# This workflow will upload a Python Package using Twine when a release is created - name: pypi-publish -# Controls when the workflow will run on: - workflow_dispatch: {} + workflow_dispatch: release: types: [ published ] jobs: - deploy: - runs-on: self-hosted + pypi-publisher: + runs-on: thevickypedia-default steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - - name: Create packages - run: python -m build - - name: Run twine check - run: twine check dist/* - - name: Upload to pypi - env: - TWINE_USERNAME: ${{ secrets.PYPI_USER }} - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: twine upload dist/*.whl + - uses: thevickypedia/pypi-publisher@v3 + env: + token: ${{ secrets.PYPI_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d908356..fce71e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,42 +1,56 @@ +--- fail_fast: true -exclude: ^docs/ +exclude: ^(notebooks/|scripts/|.github/|docs/) repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-added-large-files - - id: check-ast - - id: check-byte-order-marker - - id: check-builtin-literals - - id: check-case-conflict - - id: check-docstring-first - - id: check-executables-have-shebangs - - id: check-shebang-scripts-are-executable - - id: check-merge-conflict - - id: check-toml - - id: check-vcs-permalinks - - id: check-xml - - id: debug-statements - - id: destroyed-symlinks - - id: detect-aws-credentials - - id: detect-private-key - - id: end-of-file-fixer - - id: fix-byte-order-marker - - id: mixed-line-ending - - id: name-tests-test - - id: requirements-txt-fixer - - id: trailing-whitespace + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + - id: check-json + - id: check-added-large-files + - id: check-ast + - id: check-byte-order-marker + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: debug-statements + - id: destroyed-symlinks + - id: detect-aws-credentials + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: name-tests-test + - id: requirements-txt-fixer + - id: trailing-whitespace - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort - - repo: local - hooks: - - id: runbook - name: runbook - entry: /bin/bash gen_docs.sh - language: system - pass_filenames: false - always_run: true + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + args: [-j8, '--ignore=F401,W503,E203,E501,F821,E306,E722,N812'] + + - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt + rev: 0.2.3 + hooks: + - id: yamlfmt + + - repo: local + hooks: + - id: runbook + name: runbook + entry: /bin/bash gen_docs.sh + language: system + pass_filenames: false + always_run: true diff --git a/doc_generator/conf.py b/doc_generator/conf.py index 8a0d7e7..7f973d5 100644 --- a/doc_generator/conf.py +++ b/doc_generator/conf.py @@ -13,13 +13,13 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- -project = 'Gmail Connector' -copyright = '2021, Vignesh Rao' -author = 'Vignesh Rao' +project = "Gmail Connector" +copyright = "2021, Vignesh Rao" +author = "Vignesh Rao" # -- General configuration --------------------------------------------------- @@ -27,50 +27,52 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.napoleon', # certain styles of doc strings - 'sphinx.ext.autodoc', # generates from doc strings - 'recommonmark', # supports markdown integration + "sphinx.ext.napoleon", # certain styles of doc strings + "sphinx.ext.autodoc", # generates from doc strings + "recommonmark", # supports markdown integration ] # Exclude private members in the docs # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_default_options -autodoc_default_options = {"members": True, "undoc-members": True, "private-members": False} +autodoc_default_options = { + "members": True, + "undoc-members": True, + "private-members": False, +} # https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#configuration napoleon_google_docstring = True napoleon_use_param = False # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # https://www.sphinx-doc.org/en/master/usage/theming.html#builtin-themes -html_theme = 'classic' -html_theme_options = { - "body_max_width": "80%" -} +html_theme = "classic" +html_theme_options = {"body_max_width": "80%"} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add docstrings from __init__ method # Reference: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autoclass_content -autoclass_content = 'both' +autoclass_content = "both" # Add support to mark down files in sphinx documentation # Reference: https://www.sphinx-doc.org/en/1.5.3/markdown.html source_suffix = { - '.rst': 'restructuredtext', - '.txt': 'markdown', - '.md': 'markdown', + ".rst": "restructuredtext", + ".txt": "markdown", + ".md": "markdown", } diff --git a/docs/genindex.html b/docs/genindex.html index be678e0..ecc5a80 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -107,6 +107,8 @@

C

  • Condition (class in gmailconnector.models.options)
  • count (gmailconnector.models.responder.Response property) +
  • +
  • create_connection() (gmailconnector.send_email.SendEmail method)
  • create_ssl_connection() (gmailconnector.read_email.ReadEmail method) diff --git a/docs/index.html b/docs/index.html index 94d4978..fd9af22 100644 --- a/docs/index.html +++ b/docs/index.html @@ -61,7 +61,7 @@

    Welcome to Gmail Connector’s documentation!

    Send SMS

    -class gmailconnector.send_sms.SendSMS(**kwargs: Unpack)
    +class gmailconnector.send_sms.SendSMS

    Initiates Messenger object to send an SMS to a phone number using SMS gateway provided by the mobile carrier.

    >>> SendSMS
     
    @@ -201,7 +201,7 @@

    Welcome to Gmail Connector’s documentation!

    Send Email

    -class gmailconnector.send_email.SendEmail(**kwargs: Unpack)
    +class gmailconnector.send_email.SendEmail

    Initiates Emailer object to send an email.

    >>> SendEmail
     
    @@ -232,6 +232,12 @@

    Welcome to Gmail Connector’s documentation! +
    +create_connection() None
    +

    Creates SSL/TLS connection based on the request parameter.

    +

    +
    create_ssl_connection(host: str, timeout: Union[int, float]) None
    @@ -276,7 +282,7 @@

    Welcome to Gmail Connector’s documentation!
    -send_email(subject: str, recipient: Union[str, list], sender: str = 'GmailConnector', body: str = None, html_body: str = None, attachment: Union[str, list] = None, filename: Union[str, list] = None, custom_attachment: Dict[Union[str, PathLike], str] = None, cc: Union[str, list] = None, bcc: Union[str, list] = None, fail_if_attach_fails: bool = True) Response
    +send_email(subject: str, recipient: Union[str, list], sender: str = 'GmailConnector', body: Optional[str] = None, html_body: Optional[str] = None, attachment: Optional[Union[str, list]] = None, filename: Optional[Union[str, list]] = None, custom_attachment: Optional[Dict[Union[str, PathLike], str]] = None, cc: Optional[Union[str, list]] = None, bcc: Optional[Union[str, list]] = None, fail_if_attach_fails: bool = True) Response

    Initiates a TLS connection and sends the email.

    Parameters:
    @@ -316,7 +322,7 @@

    Welcome to Gmail Connector’s documentation!

    Read Email

    -class gmailconnector.read_email.ReadEmail(**kwargs: Unpack)
    +class gmailconnector.read_email.ReadEmail

    Initiates Emailer object to authenticate and yield the emails according the conditions/filters.

    >>> ReadEmail
     
    @@ -355,7 +361,7 @@

    Welcome to Gmail Connector’s documentation!
    -create_ssl_connection(gmail_host: str, timeout: Union[int, float]) None
    +create_ssl_connection() None

    Creates an SSL connection to gmail’s SSL server.

    @@ -424,7 +430,7 @@

    Welcome to Gmail Connector’s documentation!

    Validator

    -gmailconnector.validator.validate_email.validate_email(email_address: str, timeout: ~typing.Union[int, float] = 5, sender: str = None, debug: bool = False, smtp_check: bool = True, logger: ~logging.Logger = <Logger validator (DEBUG)>) Response
    +gmailconnector.validator.validate_email.validate_email(email_address: str, timeout: ~typing.Union[int, float] = 5, sender: ~typing.Optional[str] = None, debug: bool = False, smtp_check: bool = True, logger: ~logging.Logger = <Logger validator (DEBUG)>) Response

    Validates email address deliver-ability using SMTP.

    Parameters:
    @@ -634,7 +640,7 @@

    Welcome to Gmail Connector’s documentation!
    -class gmailconnector.models.config.Encryption(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
    +class gmailconnector.models.config.Encryption(value)

    Enum wrapper for TLS and SSL encryption.

    >>> Encryption
     
    @@ -850,7 +856,7 @@

    Welcome to Gmail Connector’s documentation!
    -class gmailconnector.models.options.Folder(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
    +class gmailconnector.models.options.Folder(value)

    Wrapper for folders to choose emails from.

    diff --git a/docs/objects.inv b/docs/objects.inv index 4e40476..08f2939 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/searchindex.js b/docs/searchindex.js index 8581b6b..8311d60 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["Gmail Connector", "Welcome to Gmail Connector\u2019s documentation!"], "terms": {"python": 0, "ani": [0, 1], "folder": [0, 1], "As": 0, "mai": 0, "30": 0, "2022": 0, "googl": 0, "longer": 0, "support": 0, "third": 0, "parti": 0, "applic": 0, "access": [0, 1], "account": 0, "onli": [0, 1], "us": [0, 1], "usernam": [0, 1], "password": [0, 1], "which": [0, 1], "wa": [0, 1], "origin": 0, "avail": [0, 1], "through": [0, 1], "lesssecureapp": 0, "an": [0, 1], "altern": 0, "approach": 0, "i": [0, 1], "gener": [0, 1], "apppassword": 0, "instead": [0, 1], "refer": [0, 1], "http": [0, 1], "com": [0, 1], "answer": [0, 1], "6010255": 0, "pip": 0, "environ": [0, 1], "variabl": [0, 1], "can": [0, 1], "load": [0, 1], "from": [0, 1], "file": [0, 1], "gmail_us": [0, 1], "gmail_pass": [0, 1], "account_password": 0, "custom": [0, 1], "To": [0, 1], "set": [0, 1], "filenam": [0, 1], "env_fil": [0, 1], "befor": 0, "import": [0, 1], "gmailconnector": [0, 1], "o": 0, "avoid": 0, "argument": [0, 1], "dure": [0, 1], "object": [0, 1], "instanti": [0, 1], "gc": 0, "kwarg": [0, 1], "dict": [0, 1], "email_address": [0, 1], "encrypt": [0, 1], "ssl": [0, 1], "timeout": [0, 1], "5": [0, 1], "email_obj": 0, "sendemail": [0, 1], "sms_object": 0, "sendsm": [0, 1], "auth": 0, "authent": [0, 1], "happen": 0, "separ": 0, "assert": 0, "ok": [0, 1], "bodi": [0, 1], "respons": [0, 1], "send_sm": [0, 1], "phone": [0, 1], "1234567890": 0, "country_cod": [0, 1], "1": [0, 1], "messag": [0, 1], "test": 0, "sms_gatewai": [0, 1], "smsgatewai": 0, "verizon": [0, 1], "delete_s": [0, 1], "true": [0, 1], "fals": [0, 1], "keep": 0, "sent": 0, "json": [0, 1], "print": 0, "more": [0, 1], "warn": 0, "": 0, "gatewai": [0, 1], "ha": [0, 1], "payload": 0, "limit": 0, "so": [0, 1], "recommend": 0, "break": 0, "larger": 0, "multipl": [0, 1], "subject": [0, 1], "default": [0, 1], "address": 0, "_": 0, "carrier": [0, 1], "tmomail": [0, 1], "net": [0, 1], "delet": 0, "boolean": [0, 1], "flag": [0, 1], "outbound": 0, "sentitem": [0, 1], "If": 0, "known": 0, "ensur": 0, "proper": 0, "deliveri": 0, "mail_object": 0, "send_email": [0, 1], "beforehand": 0, "basic": 0, "recipi": [0, 1], "howdi": 0, "verifi": 0, "requir": [0, 1], "smtp": [0, 1], "port": [0, 1], "25": [0, 1], "validation_result": 0, "validate_email": [0, 1], "someon": 0, "exampl": 0, "valid": 0, "found": 0, "elif": 0, "invalid": [0, 1], "els": 0, "incomplet": 0, "couldn": 0, "t": 0, "mostli": 0, "becaus": 0, "block": [0, 1], "isp": 0, "differ": 0, "case": 0, "add": [0, 1], "attach": [0, 1], "without": [0, 1], "imag": 0, "path": [0, 1], "join": 0, "getcwd": 0, "listdir": 0, "name": [0, 1], "appl": 0, "flower": 0, "balloon": 0, "2": [0, 1], "dictionari": [0, 1], "custom_attach": [0, 1], "zip": 0, "3": 0, "list": [0, 1], "4": 0, "singl": 0, "random_apple_xroamutiypa": 0, "jpeg": 0, "blank": 0, "html": [0, 1], "format": [0, 1], "inlin": 0, "public": 0, "src": 0, "itself": [0, 1], "sender": [0, 1], "cc": [0, 1], "whom": [0, 1], "d": [0, 1], "bcc": [0, 1], "than": [0, 1], "one": 0, "wrap": 0, "username1": 0, "username2": 0, "datetim": [0, 1], "reader": 0, "reademail": [0, 1], "all": [0, 1], "filter1": 0, "condit": [0, 1], "sinc": [0, 1], "date": [0, 1], "year": 0, "2010": 0, "month": 0, "dai": 0, "filter2": 0, "secur": 0, "alert": 0, "filter3": 0, "text": [0, 1], "filter4": 0, "categori": [0, 1], "not_delet": [0, 1], "filter": [0, 1], "appli": 0, "each_mail": 0, "read_mail": [0, 1], "humanize_datetim": [0, 1], "get": [0, 1], "date_tim": 0, "sender_email": 0, "precommit": 0, "doc": 0, "creation": 0, "ar": 0, "run": 0, "everi": 0, "commit": 0, "sphinx": 0, "pre": 0, "20": 0, "0": [0, 1], "recommonmark": 0, "7": 0, "m": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": [0, 1], "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "instal": 1, "env": 1, "var": 1, "usag": 1, "licens": 1, "copyright": 1, "class": 1, "unpack": 1, "initi": 1, "messeng": 1, "number": 1, "provid": 1, "mobil": 1, "necessari": 1, "arg": 1, "creat": 1, "connect": 1, "host": 1, "base": 1, "chosen": 1, "type": 1, "keyword": 1, "lib": 1, "gmail_host": 1, "hostnam": 1, "server": 1, "properti": 1, "return": 1, "A": 1, "statu": 1, "user": 1, "create_ssl_connect": 1, "str": 1, "union": 1, "int": 1, "float": 1, "none": 1, "create_tls_connect": 1, "tl": 1, "smsgatewaymodel": 1, "att": 1, "mm": 1, "tmobil": 1, "vtext": 1, "boost": 1, "smsmyboostmobil": 1, "cricket": 1, "cricketwireless": 1, "uscellular": 1, "uscc": 1, "bool": 1, "destin": 1, "paramet": 1, "content": 1, "countri": 1, "code": 1, "line": 1, "take": 1, "item": 1, "encod": 1, "ascii": 1, "ignor": 1, "decod": 1, "thi": 1, "done": 1, "special": 1, "charact": 1, "like": 1, "rais": 1, "unicodeencodeerror": 1, "note": 1, "other": 1, "includ": 1, "replac": 1, "xmlcharrefreplac": 1, "sms_delet": 1, "deletes": 1, "select": 1, "right": 1, "after": 1, "invok": 1, "thread_executor": 1, "sweep": 1, "time": 1, "taken": 1, "depend": 1, "exist": 1, "item_id": 1, "byte": 1, "thread": 1, "just": 1, "id": 1, "multipart_messag": 1, "html_bodi": 1, "mimemultipart": 1, "multipart": 1, "pass": 1, "given": 1, "version": 1, "pathlik": 1, "fail_if_attach_fail": 1, "filepath": 1, "kei": 1, "valu": 1, "restrict": 1, "fail": 1, "them": 1, "read_email": 1, "yield": 1, "accord": 1, "where": 1, "have": 1, "imapcli": 1, "readthedoc": 1, "en": 1, "_modul": 1, "xlist_fold": 1, "broad": 1, "claus": 1, "catch": 1, "login": 1, "error": 1, "same": 1, "imaplib": 1, "get_info": 1, "response_part": 1, "tupl": 1, "dt_flag": 1, "extract": 1, "receiv": 1, "part": 1, "whether": 1, "convert": 1, "human": 1, "readabl": 1, "inform": 1, "iter": 1, "__str__": 1, "unseen": 1, "search": 1, "form": 1, "api": 1, "contain": 1, "match": 1, "criteria": 1, "method": 1, "debug": 1, "smtp_check": 1, "logger": 1, "log": 1, "deliv": 1, "abil": 1, "second": 1, "wait": 1, "result": 1, "enabl": 1, "check": 1, "bring": 1, "your": 1, "own": 1, "clearli": 1, "mx": 1, "record": 1, "temporari": 1, "emailaddress": 1, "validateaddress": 1, "split": 1, "domin": 1, "further": 1, "idna": 1, "internation": 1, "string": 1, "ipv4address": 1, "ipv6address": 1, "get_mx_record": 1, "mail": 1, "exchang": 1, "fqdn": 1, "fulli": 1, "qualifi": 1, "ip": 1, "authorit": 1, "non": 1, "section": 1, "addressformaterror": 1, "invaliddomain": 1, "notmailserv": 1, "unresponsivemailserv": 1, "unrespons": 1, "egressconfig": 1, "_case_sensit": 1, "_env_prefix": 1, "_env_fil": 1, "dotenvtyp": 1, "posixpath": 1, "_env_file_encod": 1, "_env_nested_delimit": 1, "_secrets_dir": 1, "emailstr": 1, "10": 1, "configur": 1, "pydant": 1, "share": 1, "across": 1, "modul": 1, "new": 1, "pars": 1, "input": 1, "data": 1, "validationerror": 1, "pydantic_cor": 1, "cannot": 1, "__init__": 1, "__pydantic_self__": 1, "common": 1, "self": 1, "first": 1, "allow": 1, "field": 1, "env_prefix": 1, "extra": 1, "model_config": 1, "classvar": 1, "settingsconfigdict": 1, "arbitrary_types_allow": 1, "case_sensit": 1, "env_file_encod": 1, "env_nested_delimit": 1, "protected_namespac": 1, "model_": 1, "settings_": 1, "secrets_dir": 1, "validate_default": 1, "should": 1, "conform": 1, "configdict": 1, "model_field": 1, "fieldinfo": 1, "annot": 1, "nonetyp": 1, "metadata": 1, "pydanticgeneralmetadata": 1, "pattern": 1, "about": 1, "defin": 1, "map": 1, "__fields__": 1, "v1": 1, "qualnam": 1, "start": 1, "boundari": 1, "enum": 1, "wrapper": 1, "ingressconfig": 1, "inbox": 1, "imap": 1, "while": 1, "NOT": 1, "seen": 1, "static": 1, "retriev": 1, "small": 1, "size": 1, "smaller": 1, "particular": 1, "present": 1, "choos": 1, "draft": 1, "spam": 1, "star": 1, "trash": 1, "turn": 1, "insert": 1, "pair": 1, "member": 1, "count": 1, "un": 1, "index": 1, "page": 1}, "objects": {"gmailconnector.models": [[1, 0, 0, "-", "config"], [1, 0, 0, "-", "options"], [1, 0, 0, "-", "responder"]], "gmailconnector.models.config": [[1, 1, 1, "", "EgressConfig"], [1, 1, 1, "", "Encryption"], [1, 1, 1, "", "IngressConfig"], [1, 1, 1, "", "SMSGatewayModel"]], "gmailconnector.models.config.EgressConfig": [[1, 1, 1, "", "Config"], [1, 2, 1, "", "encryption"], [1, 2, 1, "", "gmail_host"], [1, 2, 1, "", "gmail_pass"], [1, 2, 1, "", "gmail_user"], [1, 2, 1, "", "model_config"], [1, 2, 1, "", "model_fields"], [1, 2, 1, "", "phone"], [1, 2, 1, "", "recipient"], [1, 2, 1, "", "timeout"]], "gmailconnector.models.config.EgressConfig.Config": [[1, 2, 1, "", "env_file"], [1, 2, 1, "", "env_prefix"], [1, 2, 1, "", "extra"]], "gmailconnector.models.config.Encryption": [[1, 2, 1, "", "SSL"], [1, 2, 1, "", "TLS"]], "gmailconnector.models.config.IngressConfig": [[1, 1, 1, "", "Config"], [1, 2, 1, "", "folder"], [1, 2, 1, "", "gmail_host"], [1, 2, 1, "", "gmail_pass"], [1, 2, 1, "", "gmail_user"], [1, 2, 1, "", "model_config"], [1, 2, 1, "", "model_fields"], [1, 2, 1, "", "timeout"]], "gmailconnector.models.config.IngressConfig.Config": [[1, 2, 1, "", "env_file"], [1, 2, 1, "", "env_prefix"], [1, 2, 1, "", "extra"]], "gmailconnector.models.config.SMSGatewayModel": [[1, 2, 1, "", "att"], [1, 2, 1, "", "boost"], [1, 2, 1, "", "cricket"], [1, 2, 1, "", "model_config"], [1, 2, 1, "", "model_fields"], [1, 2, 1, "", "tmobile"], [1, 2, 1, "", "uscellular"], [1, 2, 1, "", "verizon"]], "gmailconnector.models.options": [[1, 1, 1, "", "Category"], [1, 1, 1, "", "Condition"], [1, 1, 1, "", "Folder"]], "gmailconnector.models.options.Category": [[1, 2, 1, "", "all"], [1, 2, 1, "", "flagged"], [1, 2, 1, "", "not_deleted"], [1, 2, 1, "", "seen"], [1, 2, 1, "", "unseen"]], "gmailconnector.models.options.Condition": [[1, 3, 1, "", "since"], [1, 3, 1, "", "small"], [1, 3, 1, "", "subject"], [1, 3, 1, "", "text"]], "gmailconnector.models.options.Folder": [[1, 2, 1, "", "all"], [1, 2, 1, "", "drafts"], [1, 2, 1, "", "important"], [1, 2, 1, "", "inbox"], [1, 2, 1, "", "sent"], [1, 2, 1, "", "spam"], [1, 2, 1, "", "starred"], [1, 2, 1, "", "trash"]], "gmailconnector.models.responder": [[1, 1, 1, "", "Email"], [1, 1, 1, "", "Response"]], "gmailconnector.models.responder.Response": [[1, 4, 1, "", "body"], [1, 4, 1, "", "count"], [1, 4, 1, "", "extra"], [1, 3, 1, "", "json"], [1, 4, 1, "", "ok"], [1, 4, 1, "", "status"]], "gmailconnector": [[1, 0, 0, "-", "read_email"], [1, 0, 0, "-", "send_email"], [1, 0, 0, "-", "send_sms"], [1, 0, 0, "-", "sms_deleter"]], "gmailconnector.read_email": [[1, 1, 1, "", "ReadEmail"]], "gmailconnector.read_email.ReadEmail": [[1, 4, 1, "", "authenticate"], [1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "get_info"], [1, 3, 1, "", "instantiate"], [1, 3, 1, "", "read_mail"]], "gmailconnector.send_email": [[1, 1, 1, "", "SendEmail"], [1, 5, 1, "", "validate_email"]], "gmailconnector.send_email.SendEmail": [[1, 4, 1, "", "authenticate"], [1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "create_tls_connection"], [1, 3, 1, "", "multipart_message"], [1, 3, 1, "", "send_email"]], "gmailconnector.send_sms": [[1, 1, 1, "", "SendSMS"]], "gmailconnector.send_sms.SendSMS": [[1, 4, 1, "", "authenticate"], [1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "create_tls_connection"], [1, 3, 1, "", "send_sms"]], "gmailconnector.sms_deleter": [[1, 1, 1, "", "DeleteSent"]], "gmailconnector.sms_deleter.DeleteSent": [[1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "delete_sent"], [1, 3, 1, "", "thread_executor"]], "gmailconnector.validator": [[1, 0, 0, "-", "address"], [1, 0, 0, "-", "domain"], [1, 0, 0, "-", "exceptions"], [1, 0, 0, "-", "validate_email"]], "gmailconnector.validator.address": [[1, 1, 1, "", "EmailAddress"]], "gmailconnector.validator.address.EmailAddress": [[1, 4, 1, "", "domain"], [1, 4, 1, "", "email"], [1, 4, 1, "", "user"]], "gmailconnector.validator.domain": [[1, 5, 1, "", "get_mx_records"]], "gmailconnector.validator.exceptions": [[1, 6, 1, "", "AddressFormatError"], [1, 6, 1, "", "InvalidDomain"], [1, 6, 1, "", "NotMailServer"], [1, 6, 1, "", "UnresponsiveMailServer"]], "gmailconnector.validator.validate_email": [[1, 5, 1, "", "validate_email"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute", "3": "py:method", "4": "py:property", "5": "py:function", "6": "py:exception"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"], "3": ["py", "method", "Python method"], "4": ["py", "property", "Python property"], "5": ["py", "function", "Python function"], "6": ["py", "exception", "Python exception"]}, "titleterms": {"gmail": [0, 1], "connector": [0, 1], "instal": 0, "env": 0, "var": 0, "usag": 0, "send": [0, 1], "sm": [0, 1], "addit": 0, "arg": 0, "email": [0, 1], "read": [0, 1], "lint": 0, "releas": 0, "note": 0, "pypi": 0, "modul": 0, "runbook": 0, "licens": 0, "copyright": 0, "welcom": 1, "": 1, "document": 1, "me": 1, "delet": 1, "sent": 1, "valid": 1, "address": 1, "domain": 1, "except": 1, "model": 1, "config": 1, "option": 1, "respond": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["Gmail Connector", "Welcome to Gmail Connector\u2019s documentation!"], "terms": {"python": 0, "ani": [0, 1], "folder": [0, 1], "As": 0, "mai": 0, "30": 0, "2022": 0, "googl": 0, "longer": 0, "support": 0, "third": 0, "parti": 0, "applic": 0, "access": [0, 1], "account": 0, "onli": [0, 1], "us": [0, 1], "usernam": [0, 1], "password": [0, 1], "which": [0, 1], "wa": [0, 1], "origin": 0, "avail": [0, 1], "through": [0, 1], "lesssecureapp": 0, "an": [0, 1], "altern": 0, "approach": 0, "i": [0, 1], "gener": [0, 1], "apppassword": 0, "instead": [0, 1], "refer": [0, 1], "http": [0, 1], "com": [0, 1], "answer": [0, 1], "6010255": 0, "pip": 0, "environ": [0, 1], "variabl": [0, 1], "can": [0, 1], "load": [0, 1], "from": [0, 1], "file": [0, 1], "gmail_us": [0, 1], "gmail_pass": [0, 1], "account_password": 0, "custom": [0, 1], "To": [0, 1], "set": [0, 1], "filenam": [0, 1], "env_fil": [0, 1], "befor": 0, "import": [0, 1], "gmailconnector": [0, 1], "o": 0, "avoid": 0, "argument": [0, 1], "dure": [0, 1], "object": [0, 1], "instanti": [0, 1], "gc": 0, "kwarg": [0, 1], "dict": [0, 1], "email_address": [0, 1], "encrypt": [0, 1], "ssl": [0, 1], "timeout": [0, 1], "5": [0, 1], "email_obj": 0, "sendemail": [0, 1], "sms_object": 0, "sendsm": [0, 1], "auth": 0, "authent": [0, 1], "happen": 0, "separ": 0, "assert": 0, "ok": [0, 1], "bodi": [0, 1], "respons": [0, 1], "send_sm": [0, 1], "phone": [0, 1], "1234567890": 0, "country_cod": [0, 1], "1": [0, 1], "messag": [0, 1], "test": 0, "sms_gatewai": [0, 1], "smsgatewai": 0, "verizon": [0, 1], "delete_s": [0, 1], "true": [0, 1], "fals": [0, 1], "keep": 0, "sent": 0, "json": [0, 1], "print": 0, "more": [0, 1], "warn": 0, "": 0, "gatewai": [0, 1], "ha": [0, 1], "payload": 0, "limit": 0, "so": [0, 1], "recommend": 0, "break": 0, "larger": 0, "multipl": [0, 1], "subject": [0, 1], "default": [0, 1], "address": 0, "_": 0, "carrier": [0, 1], "tmomail": [0, 1], "net": [0, 1], "delet": 0, "boolean": [0, 1], "flag": [0, 1], "outbound": 0, "sentitem": [0, 1], "If": 0, "known": 0, "ensur": 0, "proper": 0, "deliveri": 0, "mail_object": 0, "send_email": [0, 1], "beforehand": 0, "basic": 0, "recipi": [0, 1], "howdi": 0, "verifi": 0, "requir": [0, 1], "smtp": [0, 1], "port": [0, 1], "25": [0, 1], "validation_result": 0, "validate_email": [0, 1], "someon": 0, "exampl": 0, "valid": 0, "found": 0, "elif": 0, "invalid": [0, 1], "els": 0, "incomplet": 0, "couldn": 0, "t": 0, "mostli": 0, "becaus": 0, "block": [0, 1], "isp": 0, "differ": 0, "case": 0, "add": [0, 1], "attach": [0, 1], "without": [0, 1], "imag": 0, "path": [0, 1], "join": 0, "getcwd": 0, "listdir": 0, "name": [0, 1], "appl": 0, "flower": 0, "balloon": 0, "2": [0, 1], "dictionari": [0, 1], "custom_attach": [0, 1], "zip": 0, "3": 0, "list": [0, 1], "4": 0, "singl": 0, "random_apple_xroamutiypa": 0, "jpeg": 0, "blank": 0, "html": [0, 1], "format": [0, 1], "inlin": 0, "public": 0, "src": 0, "itself": [0, 1], "sender": [0, 1], "cc": [0, 1], "whom": [0, 1], "d": [0, 1], "bcc": [0, 1], "than": [0, 1], "one": 0, "wrap": 0, "username1": 0, "username2": 0, "datetim": [0, 1], "reader": 0, "reademail": [0, 1], "all": [0, 1], "filter1": 0, "condit": [0, 1], "sinc": [0, 1], "date": [0, 1], "year": 0, "2010": 0, "month": 0, "dai": 0, "filter2": 0, "secur": 0, "alert": 0, "filter3": 0, "text": [0, 1], "filter4": 0, "categori": [0, 1], "not_delet": [0, 1], "filter": [0, 1], "appli": 0, "each_mail": 0, "read_mail": [0, 1], "humanize_datetim": [0, 1], "get": [0, 1], "date_tim": 0, "sender_email": 0, "precommit": 0, "doc": 0, "creation": 0, "ar": 0, "run": 0, "everi": 0, "commit": 0, "sphinx": 0, "pre": 0, "20": 0, "0": [0, 1], "recommonmark": 0, "7": 0, "m": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": [0, 1], "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "instal": 1, "env": 1, "var": 1, "usag": 1, "licens": 1, "copyright": 1, "class": 1, "initi": 1, "messeng": 1, "number": 1, "provid": 1, "mobil": 1, "necessari": 1, "arg": 1, "creat": 1, "connect": 1, "host": 1, "base": 1, "chosen": 1, "type": 1, "keyword": 1, "lib": 1, "gmail_host": 1, "hostnam": 1, "server": 1, "properti": 1, "return": 1, "A": 1, "statu": 1, "user": 1, "create_ssl_connect": 1, "str": 1, "union": 1, "int": 1, "float": 1, "none": 1, "create_tls_connect": 1, "tl": 1, "smsgatewaymodel": 1, "att": 1, "mm": 1, "tmobil": 1, "vtext": 1, "boost": 1, "smsmyboostmobil": 1, "cricket": 1, "cricketwireless": 1, "uscellular": 1, "uscc": 1, "bool": 1, "destin": 1, "paramet": 1, "content": 1, "countri": 1, "code": 1, "line": 1, "take": 1, "item": 1, "encod": 1, "ascii": 1, "ignor": 1, "decod": 1, "thi": 1, "done": 1, "special": 1, "charact": 1, "like": 1, "rais": 1, "unicodeencodeerror": 1, "note": 1, "other": 1, "includ": 1, "replac": 1, "xmlcharrefreplac": 1, "sms_delet": 1, "deletes": 1, "select": 1, "right": 1, "after": 1, "invok": 1, "thread_executor": 1, "sweep": 1, "time": 1, "taken": 1, "depend": 1, "exist": 1, "item_id": 1, "byte": 1, "thread": 1, "just": 1, "id": 1, "create_connect": 1, "request": 1, "multipart_messag": 1, "html_bodi": 1, "mimemultipart": 1, "multipart": 1, "pass": 1, "given": 1, "version": 1, "pathlik": 1, "fail_if_attach_fail": 1, "filepath": 1, "kei": 1, "valu": 1, "restrict": 1, "fail": 1, "them": 1, "read_email": 1, "yield": 1, "accord": 1, "where": 1, "have": 1, "imapcli": 1, "readthedoc": 1, "en": 1, "_modul": 1, "xlist_fold": 1, "broad": 1, "claus": 1, "catch": 1, "login": 1, "error": 1, "same": 1, "imaplib": 1, "get_info": 1, "response_part": 1, "tupl": 1, "dt_flag": 1, "extract": 1, "receiv": 1, "part": 1, "whether": 1, "convert": 1, "human": 1, "readabl": 1, "inform": 1, "iter": 1, "__str__": 1, "unseen": 1, "search": 1, "form": 1, "api": 1, "contain": 1, "match": 1, "criteria": 1, "method": 1, "debug": 1, "smtp_check": 1, "logger": 1, "log": 1, "deliv": 1, "abil": 1, "second": 1, "wait": 1, "result": 1, "enabl": 1, "check": 1, "bring": 1, "your": 1, "own": 1, "clearli": 1, "mx": 1, "record": 1, "temporari": 1, "emailaddress": 1, "validateaddress": 1, "split": 1, "domin": 1, "further": 1, "idna": 1, "internation": 1, "string": 1, "ipv4address": 1, "ipv6address": 1, "get_mx_record": 1, "mail": 1, "exchang": 1, "fqdn": 1, "fulli": 1, "qualifi": 1, "ip": 1, "authorit": 1, "non": 1, "section": 1, "addressformaterror": 1, "invaliddomain": 1, "notmailserv": 1, "unresponsivemailserv": 1, "unrespons": 1, "egressconfig": 1, "_case_sensit": 1, "_env_prefix": 1, "_env_fil": 1, "dotenvtyp": 1, "posixpath": 1, "_env_file_encod": 1, "_env_nested_delimit": 1, "_secrets_dir": 1, "emailstr": 1, "10": 1, "configur": 1, "pydant": 1, "share": 1, "across": 1, "modul": 1, "new": 1, "pars": 1, "input": 1, "data": 1, "validationerror": 1, "pydantic_cor": 1, "cannot": 1, "__init__": 1, "__pydantic_self__": 1, "common": 1, "self": 1, "first": 1, "allow": 1, "field": 1, "env_prefix": 1, "extra": 1, "model_config": 1, "classvar": 1, "settingsconfigdict": 1, "arbitrary_types_allow": 1, "case_sensit": 1, "env_file_encod": 1, "env_nested_delimit": 1, "protected_namespac": 1, "model_": 1, "settings_": 1, "secrets_dir": 1, "validate_default": 1, "should": 1, "conform": 1, "configdict": 1, "model_field": 1, "fieldinfo": 1, "annot": 1, "nonetyp": 1, "metadata": 1, "pydanticgeneralmetadata": 1, "pattern": 1, "about": 1, "defin": 1, "map": 1, "__fields__": 1, "v1": 1, "enum": 1, "wrapper": 1, "ingressconfig": 1, "inbox": 1, "imap": 1, "while": 1, "NOT": 1, "seen": 1, "static": 1, "retriev": 1, "small": 1, "size": 1, "smaller": 1, "particular": 1, "present": 1, "choos": 1, "draft": 1, "spam": 1, "star": 1, "trash": 1, "turn": 1, "insert": 1, "pair": 1, "member": 1, "count": 1, "un": 1, "index": 1, "page": 1}, "objects": {"gmailconnector.models": [[1, 0, 0, "-", "config"], [1, 0, 0, "-", "options"], [1, 0, 0, "-", "responder"]], "gmailconnector.models.config": [[1, 1, 1, "", "EgressConfig"], [1, 1, 1, "", "Encryption"], [1, 1, 1, "", "IngressConfig"], [1, 1, 1, "", "SMSGatewayModel"]], "gmailconnector.models.config.EgressConfig": [[1, 1, 1, "", "Config"], [1, 2, 1, "", "encryption"], [1, 2, 1, "", "gmail_host"], [1, 2, 1, "", "gmail_pass"], [1, 2, 1, "", "gmail_user"], [1, 2, 1, "", "model_config"], [1, 2, 1, "", "model_fields"], [1, 2, 1, "", "phone"], [1, 2, 1, "", "recipient"], [1, 2, 1, "", "timeout"]], "gmailconnector.models.config.EgressConfig.Config": [[1, 2, 1, "", "env_file"], [1, 2, 1, "", "env_prefix"], [1, 2, 1, "", "extra"]], "gmailconnector.models.config.Encryption": [[1, 2, 1, "", "SSL"], [1, 2, 1, "", "TLS"]], "gmailconnector.models.config.IngressConfig": [[1, 1, 1, "", "Config"], [1, 2, 1, "", "folder"], [1, 2, 1, "", "gmail_host"], [1, 2, 1, "", "gmail_pass"], [1, 2, 1, "", "gmail_user"], [1, 2, 1, "", "model_config"], [1, 2, 1, "", "model_fields"], [1, 2, 1, "", "timeout"]], "gmailconnector.models.config.IngressConfig.Config": [[1, 2, 1, "", "env_file"], [1, 2, 1, "", "env_prefix"], [1, 2, 1, "", "extra"]], "gmailconnector.models.config.SMSGatewayModel": [[1, 2, 1, "", "att"], [1, 2, 1, "", "boost"], [1, 2, 1, "", "cricket"], [1, 2, 1, "", "model_config"], [1, 2, 1, "", "model_fields"], [1, 2, 1, "", "tmobile"], [1, 2, 1, "", "uscellular"], [1, 2, 1, "", "verizon"]], "gmailconnector.models.options": [[1, 1, 1, "", "Category"], [1, 1, 1, "", "Condition"], [1, 1, 1, "", "Folder"]], "gmailconnector.models.options.Category": [[1, 2, 1, "", "all"], [1, 2, 1, "", "flagged"], [1, 2, 1, "", "not_deleted"], [1, 2, 1, "", "seen"], [1, 2, 1, "", "unseen"]], "gmailconnector.models.options.Condition": [[1, 3, 1, "", "since"], [1, 3, 1, "", "small"], [1, 3, 1, "", "subject"], [1, 3, 1, "", "text"]], "gmailconnector.models.options.Folder": [[1, 2, 1, "", "all"], [1, 2, 1, "", "drafts"], [1, 2, 1, "", "important"], [1, 2, 1, "", "inbox"], [1, 2, 1, "", "sent"], [1, 2, 1, "", "spam"], [1, 2, 1, "", "starred"], [1, 2, 1, "", "trash"]], "gmailconnector.models.responder": [[1, 1, 1, "", "Email"], [1, 1, 1, "", "Response"]], "gmailconnector.models.responder.Response": [[1, 4, 1, "", "body"], [1, 4, 1, "", "count"], [1, 4, 1, "", "extra"], [1, 3, 1, "", "json"], [1, 4, 1, "", "ok"], [1, 4, 1, "", "status"]], "gmailconnector": [[1, 0, 0, "-", "read_email"], [1, 0, 0, "-", "send_email"], [1, 0, 0, "-", "send_sms"], [1, 0, 0, "-", "sms_deleter"]], "gmailconnector.read_email": [[1, 1, 1, "", "ReadEmail"]], "gmailconnector.read_email.ReadEmail": [[1, 4, 1, "", "authenticate"], [1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "get_info"], [1, 3, 1, "", "instantiate"], [1, 3, 1, "", "read_mail"]], "gmailconnector.send_email": [[1, 1, 1, "", "SendEmail"], [1, 5, 1, "", "validate_email"]], "gmailconnector.send_email.SendEmail": [[1, 4, 1, "", "authenticate"], [1, 3, 1, "", "create_connection"], [1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "create_tls_connection"], [1, 3, 1, "", "multipart_message"], [1, 3, 1, "", "send_email"]], "gmailconnector.send_sms": [[1, 1, 1, "", "SendSMS"]], "gmailconnector.send_sms.SendSMS": [[1, 4, 1, "", "authenticate"], [1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "create_tls_connection"], [1, 3, 1, "", "send_sms"]], "gmailconnector.sms_deleter": [[1, 1, 1, "", "DeleteSent"]], "gmailconnector.sms_deleter.DeleteSent": [[1, 3, 1, "", "create_ssl_connection"], [1, 3, 1, "", "delete_sent"], [1, 3, 1, "", "thread_executor"]], "gmailconnector.validator": [[1, 0, 0, "-", "address"], [1, 0, 0, "-", "domain"], [1, 0, 0, "-", "exceptions"], [1, 0, 0, "-", "validate_email"]], "gmailconnector.validator.address": [[1, 1, 1, "", "EmailAddress"]], "gmailconnector.validator.address.EmailAddress": [[1, 4, 1, "", "domain"], [1, 4, 1, "", "email"], [1, 4, 1, "", "user"]], "gmailconnector.validator.domain": [[1, 5, 1, "", "get_mx_records"]], "gmailconnector.validator.exceptions": [[1, 6, 1, "", "AddressFormatError"], [1, 6, 1, "", "InvalidDomain"], [1, 6, 1, "", "NotMailServer"], [1, 6, 1, "", "UnresponsiveMailServer"]], "gmailconnector.validator.validate_email": [[1, 5, 1, "", "validate_email"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute", "3": "py:method", "4": "py:property", "5": "py:function", "6": "py:exception"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"], "3": ["py", "method", "Python method"], "4": ["py", "property", "Python property"], "5": ["py", "function", "Python function"], "6": ["py", "exception", "Python exception"]}, "titleterms": {"gmail": [0, 1], "connector": [0, 1], "instal": 0, "env": 0, "var": 0, "usag": 0, "send": [0, 1], "sm": [0, 1], "addit": 0, "arg": 0, "email": [0, 1], "read": [0, 1], "lint": 0, "releas": 0, "note": 0, "pypi": 0, "modul": 0, "runbook": 0, "licens": 0, "copyright": 0, "welcom": 1, "": 1, "document": 1, "me": 1, "delet": 1, "sent": 1, "valid": 1, "address": 1, "domain": 1, "except": 1, "model": 1, "config": 1, "option": 1, "respond": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/gmailconnector/__init__.py b/gmailconnector/__init__.py index 29d532b..822a823 100644 --- a/gmailconnector/__init__.py +++ b/gmailconnector/__init__.py @@ -1,7 +1,7 @@ """Place holder for package.""" -from .models.config import (EgressConfig, Encryption, # noqa: F401 - IngressConfig, SMSGateway) +from .models.config import Encryption # noqa: F401 +from .models.config import EgressConfig, IngressConfig, SMSGateway from .models.options import Category, Condition, Folder # noqa: F401 from .models.responder import Response # noqa: F401 from .read_email import ReadEmail # noqa: F401 @@ -11,4 +11,4 @@ from .validator.address import EmailAddress # noqa: F401 from .validator.validate_email import validate_email # noqa: F401 -version = "1.0" +version = "1.0.1" diff --git a/gmailconnector/lib/requirements.txt b/gmailconnector/lib/requirements.txt index 976cb4d..8f54e97 100644 --- a/gmailconnector/lib/requirements.txt +++ b/gmailconnector/lib/requirements.txt @@ -2,4 +2,4 @@ dnspython==2.6.1 idna==3.* pydantic[email]==2.4.* pydantic_settings==2.0.* -pytz==2023.* +pytz diff --git a/gmailconnector/models/options.py b/gmailconnector/models/options.py index 93827c8..18a8442 100644 --- a/gmailconnector/models/options.py +++ b/gmailconnector/models/options.py @@ -19,10 +19,10 @@ def text(text: str): return 'TEXT "%s"' % text @staticmethod - def since(since: Union[str, float, 'datetime.date']): + def since(since: Union[str, float, "datetime.date"]): """Condition to retrieve emails since a given date.""" if isinstance(since, datetime.date): - return 'SINCE "%s"' % since.strftime('%d-%b-%Y') + return 'SINCE "%s"' % since.strftime("%d-%b-%Y") return 'SINCE "%s"' % since @staticmethod diff --git a/gmailconnector/models/responder.py b/gmailconnector/models/responder.py index 0770723..a0ea021 100644 --- a/gmailconnector/models/responder.py +++ b/gmailconnector/models/responder.py @@ -25,7 +25,7 @@ def ok(self) -> bool: bool: ``True`` or ``False`` based on the arg value received. """ - return self.raw.get('ok') + return self.raw.get("ok") @property def status(self) -> int: @@ -35,7 +35,7 @@ def status(self) -> int: int: ``HTTP`` status code as received. """ - return self.raw.get('status') + return self.raw.get("status") @property def body(self) -> str: @@ -45,7 +45,7 @@ def body(self) -> str: str: Returns the message as received. """ - return self.raw.get('body') + return self.raw.get("body") def json(self) -> dict: """Returns a dictionary of the argument that was received during class initialization. @@ -64,7 +64,7 @@ def count(self) -> int: int: Returns the number of emails. """ - return self.raw.get('count') + return self.raw.get("count") @property def extra(self) -> Any: @@ -74,7 +74,7 @@ def extra(self) -> Any: Any: Returns information as received. """ - return self.raw.get('extra') + return self.raw.get("extra") class Email: @@ -86,8 +86,8 @@ def __init__(self, dictionary: dict): Args: dictionary: Takes the dictionary to be converted as an argument. """ - self.sender: str = dictionary['sender'] - self.sender_email: str = dictionary['sender_email'] - self.subject: str = dictionary['subject'] - self.date_time: Union[str, 'datetime'] = dictionary['date_time'] - self.body: str = dictionary['body'] + self.sender: str = dictionary["sender"] + self.sender_email: str = dictionary["sender_email"] + self.subject: str = dictionary["subject"] + self.date_time: Union[str, "datetime"] = dictionary["date_time"] + self.body: str = dictionary["body"] diff --git a/gmailconnector/read_email.py b/gmailconnector/read_email.py index ff61208..e7cfe1a 100644 --- a/gmailconnector/read_email.py +++ b/gmailconnector/read_email.py @@ -27,7 +27,7 @@ class ReadEmail: LOCAL_TIMEZONE = datetime.now(timezone.utc).astimezone().tzinfo - def __init__(self, **kwargs: 'Unpack[IngressConfig]'): + def __init__(self, **kwargs: "Unpack[IngressConfig]"): """Loads all the necessary args, creates a connection with Gmail host to read emails from the chosen folder. Keyword Args: @@ -46,14 +46,14 @@ def __init__(self, **kwargs: 'Unpack[IngressConfig]'): self.error, self.mail = None, None self._authenticated = False self.env = IngressConfig(**kwargs) - self.create_ssl_connection(gmail_host=self.env.gmail_host, timeout=self.env.timeout) + self.create_ssl_connection() - def create_ssl_connection(self, - gmail_host: str, - timeout: Union[int, float]) -> None: + def create_ssl_connection(self) -> None: """Creates an SSL connection to gmail's SSL server.""" try: - self.mail = imaplib.IMAP4_SSL(host=gmail_host, port=993, timeout=timeout) + self.mail = imaplib.IMAP4_SSL( + host=self.env.gmail_host, port=993, timeout=self.env.timeout + ) except socket.error as error: self.error = error.__str__() @@ -66,31 +66,34 @@ def authenticate(self) -> Response: A custom response object with properties: ok, status and body to the user. """ if self.mail is None: - return Response(dictionary={ - 'ok': False, - 'status': 408, - 'body': self.error or "failed to create a connection with gmail's SMTP server" - }) + return Response( + dictionary={ + "ok": False, + "status": 408, + "body": self.error + or "failed to create a connection with gmail's SMTP server", + } + ) try: self.mail.login(user=self.env.gmail_user, password=self.env.gmail_pass) self.mail.list() # list all the folders within your mailbox (like inbox, sent, drafts, etc) self.mail.select(self.env.folder) self._authenticated = True - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': 'authentication success' - }) + return Response( + dictionary={"ok": True, "status": 200, "body": "authentication success"} + ) except Exception as error: self.error = error.__str__() - return Response(dictionary={ - 'ok': False, - 'status': 401, - 'body': 'authentication failed' - }) - - def instantiate(self, - filters: Union[Iterable[Category.__str__], Iterable[Condition.__str__]] = "UNSEEN") -> Response: + return Response( + dictionary={"ok": False, "status": 401, "body": "authentication failed"} + ) + + def instantiate( + self, + filters: Union[ + Iterable[Category.__str__], Iterable[Condition.__str__] + ] = "UNSEEN", + ) -> Response: """Searches the number of emails for the category received and forms. Args: @@ -108,36 +111,35 @@ def instantiate(self, if not status.ok: return status if type(filters) in (list, tuple): - filters = ' '.join(filters) + filters = " ".join(filters) return_code, messages = self.mail.search(None, filters) - if return_code != 'OK': - return Response(dictionary={ - 'ok': False, - 'status': 404, - 'body': 'Unable to read emails.' - }) + if return_code != "OK": + return Response( + dictionary={ + "ok": False, + "status": 404, + "body": "Unable to read emails.", + } + ) num = len(messages[0].split()) if not num: - return Response(dictionary={ - 'ok': False, - 'status': 204, - 'body': f'No emails found in {self.env.gmail_user} [{self.env.folder}] ' - f'for the filter(s) {filters.lower()!r}', - 'count': num - }) - - if return_code == 'OK': - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': messages, - 'count': num - }) - - def get_info(self, - response_part: tuple, - dt_flag: bool) -> Email: + return Response( + dictionary={ + "ok": False, + "status": 204, + "body": f"No emails found in {self.env.gmail_user} [{self.env.folder}] " + f"for the filter(s) {filters.lower()!r}", + "count": num, + } + ) + + if return_code == "OK": + return Response( + dictionary={"ok": True, "status": 200, "body": messages, "count": num} + ) + + def get_info(self, response_part: tuple, dt_flag: bool) -> Email: """Extracts sender, subject, body and time received from response part. Args: @@ -149,21 +151,26 @@ def get_info(self, Email object with information. """ original_email = email.message_from_bytes(response_part[1]) - if received := original_email.get('Received'): - date = received.split(';')[-1].strip() + if received := original_email.get("Received"): + date = received.split(";")[-1].strip() else: - date = original_email.get('Date') - if '(PDT)' in date: + date = original_email.get("Date") + if "(PDT)" in date: datetime_obj = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S -0700 (PDT)") - elif '(PST)' in date: + elif "(PST)" in date: datetime_obj = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S -0800 (PST)") else: datetime_obj = datetime.now() - from_ = original_email['From'].split(' <') - sub = make_header(decode_header(original_email['Subject'])) \ - if original_email['Subject'] else None + from_ = original_email["From"].split(" <") + sub = ( + make_header(decode_header(original_email["Subject"])) + if original_email["Subject"] + else None + ) # Converts pacific time to local timezone as the default is pacific - local_time = datetime_obj.replace(tzinfo=pytz.timezone('US/Pacific')).astimezone(tz=self.LOCAL_TIMEZONE) + local_time = datetime_obj.replace( + tzinfo=pytz.timezone("US/Pacific") + ).astimezone(tz=self.LOCAL_TIMEZONE) if dt_flag: received_date = local_time.strftime("%Y-%m-%d") current_date_ = datetime.today().date() @@ -176,9 +183,11 @@ def get_info(self, receive = local_time.strftime("on %A, %B %d, at %I:%M %p") else: receive = local_time - if original_email.get_content_type() == "text/plain": # ignore attachments and html + if ( + original_email.get_content_type() == "text/plain" + ): # ignore attachments and html body = original_email.get_payload(decode=True) - body = body.decode('utf-8') + body = body.decode("utf-8") else: body = "" for payload in original_email.get_payload(): @@ -191,25 +200,38 @@ def get_info(self, decoded = base64.b64decode(payload) except binascii.Error: try: - decoded = payload.decode() # encoding is unknown at this point so default to UTF-8 + decoded = ( + payload.decode() + ) # encoding is unknown at this point so default to UTF-8 except UnicodeDecodeError: - warnings.warn( - "Unknown encoding type for payload" - ) + warnings.warn("Unknown encoding type for payload") continue body += decoded else: - warnings.warn( - f"Unsupported payload type: {type(payload)}" - ) + warnings.warn(f"Unsupported payload type: {type(payload)}") if len(from_) == 1: - return Email(dictionary=dict(sender=None, sender_email=from_[0].lstrip('<').rstrip('>'), - subject=sub, date_time=receive, body=body)) - return Email(dictionary=dict(sender=from_[0], sender_email=from_[1].rstrip('>'), - subject=sub, date_time=receive, body=body)) - - def read_mail(self, messages: Union[list, str], - humanize_datetime: bool = False) -> Generator[Email]: + return Email( + dictionary=dict( + sender=None, + sender_email=from_[0].lstrip("<").rstrip(">"), + subject=sub, + date_time=receive, + body=body, + ) + ) + return Email( + dictionary=dict( + sender=from_[0], + sender_email=from_[1].rstrip(">"), + subject=sub, + date_time=receive, + body=body, + ) + ) + + def read_mail( + self, messages: Union[list, str], humanize_datetime: bool = False + ) -> Generator[Email]: """Yield emails matching the filters' criteria. Args: @@ -221,10 +243,12 @@ def read_mail(self, messages: Union[list, str], A custom response object with properties: ok, status and body to the user. """ for nm in messages[0].split(): - dummy, data = self.mail.fetch(nm, '(RFC822)') + dummy, data = self.mail.fetch(nm, "(RFC822)") for each_response in data: if isinstance(each_response, tuple): - yield self.get_info(response_part=each_response, dt_flag=humanize_datetime) + yield self.get_info( + response_part=each_response, dt_flag=humanize_datetime + ) else: if self.mail: self.mail.close() diff --git a/gmailconnector/send_email.py b/gmailconnector/send_email.py index 510f5ce..de01c47 100644 --- a/gmailconnector/send_email.py +++ b/gmailconnector/send_email.py @@ -28,7 +28,7 @@ class SendEmail: """ - def __init__(self, **kwargs: 'Unpack[EgressConfig]'): + def __init__(self, **kwargs: "Unpack[EgressConfig]"): """Loads all the necessary args, creates a connection with Gmail host based on chosen encryption type. Keyword Args: @@ -42,10 +42,18 @@ def __init__(self, **kwargs: 'Unpack[EgressConfig]'): self.env = EgressConfig(**kwargs) self._failed_attachments = {"FILE NOT FOUND": [], "FILE SIZE OVER 25 MB": []} self._authenticated = False + self.create_connection() + + def create_connection(self) -> None: + """Creates SSL/TLS connection based on the request parameter.""" if self.env.encryption == Encryption.TLS: - self.create_tls_connection(host=self.env.gmail_host, timeout=self.env.timeout) + self.create_tls_connection( + host=self.env.gmail_host, timeout=self.env.timeout + ) else: - self.create_ssl_connection(host=self.env.gmail_host, timeout=self.env.timeout) + self.create_ssl_connection( + host=self.env.gmail_host, timeout=self.env.timeout + ) def create_ssl_connection(self, host: str, timeout: Union[int, float]) -> None: """Create a connection using SSL encryption.""" @@ -54,9 +62,7 @@ def create_ssl_connection(self, host: str, timeout: Union[int, float]) -> None: except (smtplib.SMTPException, socket.error) as error: self.error = error.__str__() - def create_tls_connection(self, - host: str, - timeout: Union[int, float]) -> None: + def create_tls_connection(self, host: str, timeout: Union[int, float]) -> None: """Create a connection using TLS encryption.""" try: self.server = smtplib.SMTP(host=host, port=587, timeout=timeout) @@ -73,46 +79,45 @@ def authenticate(self) -> Response: A custom response object with properties: ok, status and body to the user. """ if self.server is None: - return Response(dictionary={ - 'ok': False, - 'status': 408, - 'body': self.error or "failed to create a connection with gmail's SMTP server" - }) + return Response( + dictionary={ + "ok": False, + "status": 408, + "body": self.error + or "failed to create a connection with gmail's SMTP server", + } + ) try: self.server.login(user=self.env.gmail_user, password=self.env.gmail_pass) self._authenticated = True - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': 'authentication success' - }) + return Response( + dictionary={"ok": True, "status": 200, "body": "authentication success"} + ) except smtplib.SMTPAuthenticationError: - return Response(dictionary={ - 'ok': False, - 'status': 401, - 'body': 'authentication failed' - }) + return Response( + dictionary={"ok": False, "status": 401, "body": "authentication failed"} + ) except smtplib.SMTPException as error: - return Response(dictionary={ - 'ok': False, - 'status': 503, - 'body': error.__str__() - }) + return Response( + dictionary={"ok": False, "status": 503, "body": error.__str__()} + ) def __del__(self): """Destructor has been called to close the connection and logout.""" if self.server: self.server.close() - def multipart_message(self, - subject: str, - recipient: Union[str, List[str]], - sender: str, - body: str, - html_body: str, - attachments: list, - filenames: list, - cc: Union[str, List[str]]) -> MIMEMultipart: + def multipart_message( + self, + subject: str, + recipient: Union[str, List[str]], + sender: str, + body: str, + html_body: str, + attachments: list, + filenames: list, + cc: Union[str, List[str]], + ) -> MIMEMultipart: """Creates a multipart message with subject, body, from and to address, and attachment if filename is passed. Args: @@ -136,58 +141,66 @@ def multipart_message(self, cc = [cc] if cc and isinstance(cc, str) else cc msg = MIMEMultipart() - msg['Subject'] = subject - msg['From'] = f"{sender} <{self.env.gmail_user}>" - msg['To'] = ','.join(recipient) + msg["Subject"] = subject + msg["From"] = f"{sender} <{self.env.gmail_user}>" + msg["To"] = ",".join(recipient) if cc: - msg['Cc'] = ','.join(cc) + msg["Cc"] = ",".join(cc) if body: - msg.attach(payload=MIMEText(body, 'plain')) + msg.attach(payload=MIMEText(body, "plain")) if html_body: - msg.attach(payload=MIMEText(html_body, 'html')) + msg.attach(payload=MIMEText(html_body, "html")) for index, attachment_ in enumerate(attachments): - file_type = attachment_.split('.')[-1] + file_type = attachment_.split(".")[-1] try: filename = filenames[index] except IndexError: filename = None - if filename and '.' in filename: # filename is passed with an extn + if filename and "." in filename: # filename is passed with an extn pass - elif filename and '.' in attachment_: # file name's extn is got from attachment name - filename = f'{filename}.{file_type}' - elif filename: # filename is passed without an extn so proceeding with the same + elif ( + filename and "." in attachment_ + ): # file name's extn is got from attachment name + filename = f"{filename}.{file_type}" + elif ( + filename + ): # filename is passed without an extn so proceeding with the same pass else: - filename = attachment_.split(os.path.sep)[-1].strip() # rips path from attachment as filename + filename = attachment_.split(os.path.sep)[ + -1 + ].strip() # rips path from attachment as filename if not os.path.isfile(attachment_): self._failed_attachments["FILE NOT FOUND"].append(filename) continue - if os.path.getsize(attachment_) / 1e+6 > 25: + if os.path.getsize(attachment_) / 1e6 > 25: self._failed_attachments["FILE SIZE OVER 25 MB"].append(filename) continue - with open(attachment_, 'rb') as file: + with open(attachment_, "rb") as file: attribute = MIMEApplication(file.read(), _subtype=file_type) - attribute.add_header('Content-Disposition', 'attachment', filename=filename) + attribute.add_header("Content-Disposition", "attachment", filename=filename) msg.attach(payload=attribute) return msg - def send_email(self, - subject: str, - recipient: Union[str, list], - sender: str = 'GmailConnector', - body: str = None, - html_body: str = None, - attachment: Union[str, list] = None, - filename: Union[str, list] = None, - custom_attachment: Dict[Union[str, os.PathLike], str] = None, - cc: Union[str, list] = None, - bcc: Union[str, list] = None, - fail_if_attach_fails: bool = True) -> Response: + def send_email( + self, + subject: str, + recipient: Union[str, list], + sender: str = "GmailConnector", + body: str = None, + html_body: str = None, + attachment: Union[str, list] = None, + filename: Union[str, list] = None, + custom_attachment: Dict[Union[str, os.PathLike], str] = None, + cc: Union[str, list] = None, + bcc: Union[str, list] = None, + fail_if_attach_fails: bool = True, + ) -> Response: """Initiates a TLS connection and sends the email. Args: @@ -218,19 +231,37 @@ def send_email(self, attachments = list(custom_attachment.keys()) filenames = list(custom_attachment.values()) else: - attachments = [attachment] if isinstance(attachment, str) else attachment if attachment else [] - filenames = [filename] if isinstance(filename, str) else filename if filename else [] + attachments = ( + [attachment] + if isinstance(attachment, str) + else attachment if attachment else [] + ) + filenames = ( + [filename] + if isinstance(filename, str) + else filename if filename else [] + ) - msg = self.multipart_message(subject=subject, sender=sender, recipient=recipient, attachments=attachments, - body=body, html_body=html_body, cc=cc, filenames=filenames) + msg = self.multipart_message( + subject=subject, + sender=sender, + recipient=recipient, + attachments=attachments, + body=body, + html_body=html_body, + cc=cc, + filenames=filenames, + ) - unattached = {k: ', '.join(v) for k, v in self._failed_attachments.items() if v} + unattached = {k: ", ".join(v) for k, v in self._failed_attachments.items() if v} if fail_if_attach_fails and unattached: - return Response(dictionary={ - 'ok': False, - 'status': 422, - 'body': f"Email was not sent. Unattached: {unattached!r}" - }) + return Response( + dictionary={ + "ok": False, + "status": 422, + "body": f"Email was not sent. Unattached: {unattached!r}", + } + ) recipients = [recipient] if isinstance(recipient, str) else recipient if cc: @@ -240,9 +271,7 @@ def send_email(self, for i in range(3): try: self.server.sendmail( - from_addr=sender, - to_addrs=recipients, - msg=msg.as_string() + from_addr=sender, to_addrs=recipients, msg=msg.as_string() ) break except smtplib.SMTPServerDisconnected as err: @@ -250,14 +279,18 @@ def send_email(self, raise err continue if unattached: - return Response(dictionary={ - 'ok': True, - 'status': 206, - 'body': f"Email has been sent to {recipient!r}. Unattached: {unattached!r}." - }) + return Response( + dictionary={ + "ok": True, + "status": 206, + "body": f"Email has been sent to {recipient!r}. Unattached: {unattached!r}.", + } + ) else: - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': f"Email has been sent to {recipient!r}" - }) + return Response( + dictionary={ + "ok": True, + "status": 200, + "body": f"Email has been sent to {recipient!r}", + } + ) diff --git a/gmailconnector/send_sms.py b/gmailconnector/send_sms.py index bf66a0c..3f262ed 100644 --- a/gmailconnector/send_sms.py +++ b/gmailconnector/send_sms.py @@ -19,7 +19,7 @@ class SendSMS: """ - def __init__(self, **kwargs: 'Unpack[EgressConfig]'): + def __init__(self, **kwargs: "Unpack[EgressConfig]"): """Loads all the necessary args, creates a connection with Gmail host based on chosen encryption type. Keyword Args: @@ -33,22 +33,22 @@ def __init__(self, **kwargs: 'Unpack[EgressConfig]'): self.env = EgressConfig(**kwargs) self._authenticated = False if self.env.encryption == Encryption.TLS: - self.create_tls_connection(host=self.env.gmail_host, timeout=self.env.timeout) + self.create_tls_connection( + host=self.env.gmail_host, timeout=self.env.timeout + ) else: - self.create_ssl_connection(host=self.env.gmail_host, timeout=self.env.timeout) + self.create_ssl_connection( + host=self.env.gmail_host, timeout=self.env.timeout + ) - def create_ssl_connection(self, - host: str, - timeout: Union[int, float]) -> None: + def create_ssl_connection(self, host: str, timeout: Union[int, float]) -> None: """Create a connection using SSL encryption.""" try: self.server = smtplib.SMTP_SSL(host=host, port=465, timeout=timeout) except (smtplib.SMTPException, socket.error) as error: self.error = error.__str__() - def create_tls_connection(self, - host: str, - timeout: Union[int, float]) -> None: + def create_tls_connection(self, host: str, timeout: Union[int, float]) -> None: """Create a connection using TLS encryption.""" try: self.server = smtplib.SMTP(host=host, port=587, timeout=timeout) @@ -65,44 +65,43 @@ def authenticate(self) -> Response: A custom response object with properties: ok, status and body to the user. """ if self.server is None: - return Response(dictionary={ - 'ok': False, - 'status': 408, - 'body': self.error or "failed to create a connection with gmail's SMTP server" - }) + return Response( + dictionary={ + "ok": False, + "status": 408, + "body": self.error + or "failed to create a connection with gmail's SMTP server", + } + ) try: self.server.login(user=self.env.gmail_user, password=self.env.gmail_pass) self._authenticated = True - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': 'authentication success' - }) + return Response( + dictionary={"ok": True, "status": 200, "body": "authentication success"} + ) except smtplib.SMTPAuthenticationError: - return Response(dictionary={ - 'ok': False, - 'status': 401, - 'body': 'authentication failed' - }) + return Response( + dictionary={"ok": False, "status": 401, "body": "authentication failed"} + ) except smtplib.SMTPException as error: - return Response(dictionary={ - 'ok': False, - 'status': 503, - 'body': error.__str__() - }) + return Response( + dictionary={"ok": False, "status": 503, "body": error.__str__()} + ) def __del__(self): """Destructor has been called to close the connection and logout.""" if self.server: self.server.close() - def send_sms(self, - message: str, - phone: str = None, - country_code: str = None, - subject: str = None, - sms_gateway: SMSGateway = None, - delete_sent: bool = False) -> Response: + def send_sms( + self, + message: str, + phone: str = None, + country_code: str = None, + subject: str = None, + sms_gateway: SMSGateway = None, + delete_sent: bool = False, + ) -> Response: """Initiates an SMTP connection and sends a text message through SMS gateway of destination number. Args: @@ -141,44 +140,54 @@ def send_sms(self, raise ValueError( f"\n\tcountry code should match the pattern {COUNTRY_CODE.pattern}" ) - body = f'\n{message}'.encode('ascii', 'ignore').decode('ascii') + body = f"\n{message}".encode("ascii", "ignore").decode("ascii") subject = subject or f"Message from {self.env.gmail_user}" if not self._authenticated: status = self.authenticate if not status.ok: return status - message = (f"From: {self.env.gmail_user}\n" + f"To: {to}\n" + f"Subject: {subject}\n" + body) - if len(message) > 428: - return Response(dictionary={ - 'ok': False, - 'status': 413, - 'body': f'Payload length: {len(message):,}, which is more than the optimal size: 428. ' - f'Message length: {len(body):,}' - }) - - self.server.sendmail( - from_addr=self.env.gmail_user, - to_addrs=to, - msg=message + message = ( + f"From: {self.env.gmail_user}\n" + + f"To: {to}\n" + + f"Subject: {subject}\n" + + body ) + if len(message) > 428: + return Response( + dictionary={ + "ok": False, + "status": 413, + "body": f"Payload length: {len(message):,}, which is more than the optimal size: 428. " + f"Message length: {len(body):,}", + } + ) + + self.server.sendmail(from_addr=self.env.gmail_user, to_addrs=to, msg=message) if delete_sent: - if delete_response := DeleteSent(username=self.env.gmail_user, password=self.env.gmail_pass, - subject=subject, body=body, to=to).delete_sent(): - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': f'SMS has been sent to {to}', - 'extra': delete_response - }) - return Response(dictionary={ - 'ok': True, - 'status': 206, - 'body': f'SMS has been sent to {to}', - 'extra': 'Failed to locate and delete the SMS from Sent Mail.' - }) - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': f'SMS has been sent to {to}' - }) + if delete_response := DeleteSent( + username=self.env.gmail_user, + password=self.env.gmail_pass, + subject=subject, + body=body, + to=to, + ).delete_sent(): + return Response( + dictionary={ + "ok": True, + "status": 200, + "body": f"SMS has been sent to {to}", + "extra": delete_response, + } + ) + return Response( + dictionary={ + "ok": True, + "status": 206, + "body": f"SMS has been sent to {to}", + "extra": "Failed to locate and delete the SMS from Sent Mail.", + } + ) + return Response( + dictionary={"ok": True, "status": 200, "body": f"SMS has been sent to {to}"} + ) diff --git a/gmailconnector/sms_deleter.py b/gmailconnector/sms_deleter.py index 8009e87..129a578 100644 --- a/gmailconnector/sms_deleter.py +++ b/gmailconnector/sms_deleter.py @@ -24,11 +24,11 @@ def __init__(self, **kwargs): body: Body of the email to be deleted. to: To address of the email to be deleted. """ - self.username = kwargs.get('username') - self.password = kwargs.get('password') - self.subject = kwargs.get('subject') - self.body = kwargs.get('body') - self.to = kwargs.get('to') + self.username = kwargs.get("username") + self.password = kwargs.get("password") + self.subject = kwargs.get("subject") + self.body = kwargs.get("body") + self.to = kwargs.get("to") self.mail = None self.error = None self.create_ssl_connection() @@ -36,34 +36,44 @@ def __init__(self, **kwargs): def create_ssl_connection(self) -> None: """Creates a connection using SSL encryption and selects the sent folder.""" try: - self.mail = imaplib.IMAP4_SSL('imap.gmail.com') + self.mail = imaplib.IMAP4_SSL("imap.gmail.com") self.mail.login(user=self.username, password=self.password) self.mail.list() self.mail.select(Folder.sent) except Exception as error: self.error = error.__str__() - def thread_executor(self, - item_id: Union[bytes, str]) -> Dict[str, str]: + def thread_executor(self, item_id: Union[bytes, str]) -> Dict[str, str]: """Gets invoked in multiple threads, to set the flag as ``Deleted`` for the message which was just sent. Args: item_id: Takes the ID of the message as an argument. """ - dummy, data = self.mail.fetch(item_id, '(RFC822)') + dummy, data = self.mail.fetch(item_id, "(RFC822)") for response_part in data: if not isinstance(response_part, tuple): continue - original_email = email.message_from_bytes(response_part[1]) # gets the raw content - sender = str(make_header(decode_header((original_email['From']).split(' <')[0]))) - sub = str(make_header(decode_header(original_email['Subject']))) - to = str(make_header(decode_header(original_email['To']))) - if to == self.to and sub == self.subject and sender == self.username and \ - original_email.__dict__.get('_payload', '').strip() == self.body.strip(): - self.mail.store(item_id.decode('UTF-8'), '+FLAGS', '\\Deleted') + original_email = email.message_from_bytes( + response_part[1] + ) # gets the raw content + sender = str( + make_header(decode_header((original_email["From"]).split(" <")[0])) + ) + sub = str(make_header(decode_header(original_email["Subject"]))) + to = str(make_header(decode_header(original_email["To"]))) + if ( + to == self.to + and sub == self.subject + and sender == self.username + and original_email.__dict__.get("_payload", "").strip() + == self.body.strip() + ): + self.mail.store(item_id.decode("UTF-8"), "+FLAGS", "\\Deleted") self.mail.expunge() - return dict(msg_id=original_email['Message-ID'], - msg_context=" ".join(original_email['Received'].split())) + return dict( + msg_id=original_email["Message-ID"], + msg_context=" ".join(original_email["Received"].split()), + ) def delete_sent(self) -> Union[Dict[str, str], None]: """Deletes the email from GMAIL's sent items right after sending the message. @@ -76,12 +86,18 @@ def delete_sent(self) -> Union[Dict[str, str], None]: """ if self.mail is None: return - return_code, messages = self.mail.search(None, 'ALL') # Includes SEEN and UNSEEN, although sent is always SEEN - if return_code != 'OK': + return_code, messages = self.mail.search( + None, "ALL" + ) # Includes SEEN and UNSEEN, although sent is always SEEN + if return_code != "OK": return with ThreadPoolExecutor(max_workers=1) as executor: - for deleted in executor.map(self.thread_executor, sorted(messages[0].split(), reverse=True)): - if deleted: # Indicates the message sent has been deleted, so no need to loop through entire sent items + for deleted in executor.map( + self.thread_executor, sorted(messages[0].split(), reverse=True) + ): + if ( + deleted + ): # Indicates the message sent has been deleted, so no need to loop through entire sent items executor.shutdown(cancel_futures=True) self.mail.close() self.mail.logout() diff --git a/gmailconnector/validator/address.py b/gmailconnector/validator/address.py index dc24ad4..e12d589 100644 --- a/gmailconnector/validator/address.py +++ b/gmailconnector/validator/address.py @@ -14,8 +14,7 @@ class EmailAddress: """ - def __init__(self, - address: str): + def __init__(self, address: str): """Converts address into IDNA (Internationalized Domain Name) format. Args: @@ -23,13 +22,13 @@ def __init__(self, """ self._address = address try: - self._user, self._domain = self._address.rsplit('@', 1) + self._user, self._domain = self._address.rsplit("@", 1) except ValueError: raise AddressFormatError if not self._user: raise AddressFormatError("Empty user") try: - self._domain = idna_encode(self._domain).decode('ascii') + self._domain = idna_encode(self._domain).decode("ascii") except IDNAError as error: raise AddressFormatError(error) @@ -46,4 +45,4 @@ def domain(self) -> Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]: @property def email(self) -> str: """Returns the email address.""" - return '@'.join((self.user, self.domain)) + return "@".join((self.user, self.domain)) diff --git a/gmailconnector/validator/domain.py b/gmailconnector/validator/domain.py index 31a6767..4f7e2c1 100644 --- a/gmailconnector/validator/domain.py +++ b/gmailconnector/validator/domain.py @@ -9,11 +9,12 @@ from .exceptions import InvalidDomain, NotMailServer, UnresponsiveMailServer -default_logger = logging.getLogger('validator') +default_logger = logging.getLogger("validator") -def get_mx_records(domain: str, - logger: logging.Logger = default_logger) -> Generator[Union[str, IPv4Address, IPv6Address]]: +def get_mx_records( + domain: str, logger: logging.Logger = default_logger +) -> Generator[Union[str, IPv4Address, IPv6Address]]: """Get MX (Mail Exchange server) records for the given domain. Args: @@ -25,7 +26,7 @@ def get_mx_records(domain: str, IP addresses of the mail exchange servers from authoritative/non-authoritative answer section. """ try: - resolved: Iterable[Answer] = resolve(domain, 'MX') + resolved: Iterable[Answer] = resolve(domain, "MX") except NXDOMAIN as error: raise InvalidDomain(error) except NoAnswer as error: @@ -34,8 +35,10 @@ def get_mx_records(domain: str, raise NotMailServer(f"Domain {domain!r} is not a mail server.") for record in resolved: record: MX = record - if record.exchange.to_text().strip() == '.': - raise UnresponsiveMailServer(f"Domain {domain!r} appears to be valid, but failed to resolve IP addresses.") + if record.exchange.to_text().strip() == ".": + raise UnresponsiveMailServer( + f"Domain {domain!r} appears to be valid, but failed to resolve IP addresses." + ) try: ip = socket.gethostbyname(record.exchange.to_text()) except socket.error as error: @@ -43,6 +46,8 @@ def get_mx_records(domain: str, raise UnresponsiveMailServer(error) except UnicodeError as error: logger.error(error) - raise UnresponsiveMailServer(f"Domain {domain!r} appears to be valid, but failed to resolve IP addresses.") + raise UnresponsiveMailServer( + f"Domain {domain!r} appears to be valid, but failed to resolve IP addresses." + ) logger.info(f"{record.preference}\t{record.exchange}\t{ip}") yield ip diff --git a/gmailconnector/validator/validate_email.py b/gmailconnector/validator/validate_email.py index 55b7546..ef6363b 100644 --- a/gmailconnector/validator/validate_email.py +++ b/gmailconnector/validator/validate_email.py @@ -10,22 +10,24 @@ from .exceptions import (AddressFormatError, InvalidDomain, NotMailServer, UnresponsiveMailServer) -formatter = logging.Formatter(fmt='%(levelname)s\t %(message)s') +formatter = logging.Formatter(fmt="%(levelname)s\t %(message)s") handler = logging.StreamHandler() handler.setFormatter(fmt=formatter) -default_logger = logging.getLogger('validator') +default_logger = logging.getLogger("validator") default_logger.addHandler(hdlr=handler) default_logger.setLevel(level=logging.DEBUG) -def validate_email(email_address: str, - timeout: Union[int, float] = 5, - sender: str = None, - debug: bool = False, - smtp_check: bool = True, - logger: logging.Logger = default_logger) -> Response: +def validate_email( + email_address: str, + timeout: Union[int, float] = 5, + sender: str = None, + debug: bool = False, + smtp_check: bool = True, + logger: logging.Logger = default_logger, +) -> Response: """Validates email address deliver-ability using SMTP. Args: @@ -51,40 +53,48 @@ def validate_email(email_address: str, try: address = EmailAddress(address=email_address) except AddressFormatError as error: - return Response(dictionary={ - 'ok': False, - 'status': 422, - 'body': f"Invalid address: {email_address!r}. {error}" if str(error).strip() else - f"Invalid address: {email_address!r}." - }) + return Response( + dictionary={ + "ok": False, + "status": 422, + "body": ( + f"Invalid address: {email_address!r}. {error}" + if str(error).strip() + else f"Invalid address: {email_address!r}." + ), + } + ) if not smtp_check: try: list(get_mx_records(domain=address.domain)) except (InvalidDomain, NotMailServer, UnresponsiveMailServer) as error: logger.error(error) - return Response(dictionary={ - 'ok': False, - 'status': 422, - 'body': error.__str__() - }) - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': f'{address.email!r} is valid' - }) + return Response( + dictionary={"ok": False, "status": 422, "body": error.__str__()} + ) + return Response( + dictionary={ + "ok": True, + "status": 200, + "body": f"{address.email!r} is valid", + } + ) try: server = smtplib.SMTP(timeout=timeout) except (smtplib.SMTPException, socket.error) as error: - return Response(dictionary={ - 'ok': False, - 'status': 408, - 'body': error.__str__() or "failed to create a connection with gmail's SMTP server" - }) + return Response( + dictionary={ + "ok": False, + "status": 408, + "body": error.__str__() + or "failed to create a connection with gmail's SMTP server", + } + ) try: for record in get_mx_records(domain=address.domain): - logger.info(f'Trying {record}...') + logger.info(f"Trying {record}...") try: server.connect(host=record) except socket.error as error: @@ -93,33 +103,35 @@ def validate_email(email_address: str, server.ehlo_or_helo_if_needed() server.mail(sender=sender or address.email) code, msg = server.rcpt(recip=address.email) - msg = re.sub(r"\d+.\d+.\d+", '', msg.decode(encoding='utf-8')).strip() - msg = ' '.join(msg.splitlines()).replace(' ', ' ').strip() if msg else "Unknown error" + msg = re.sub(r"\d+.\d+.\d+", "", msg.decode(encoding="utf-8")).strip() + msg = ( + " ".join(msg.splitlines()).replace(" ", " ").strip() + if msg + else "Unknown error" + ) if code == 550: # Definitely invalid email address - logger.info(f'Invalid email address: {address.email}') - return Response(dictionary={ - 'ok': False, - 'status': 550, - 'body': msg - }) + logger.info(f"Invalid email address: {address.email}") + return Response(dictionary={"ok": False, "status": 550, "body": msg}) if code < 400: # Valid email address - logger.info(f'Valid email address: {address.email}') - return Response(dictionary={ - 'ok': True, - 'status': 200, - 'body': f"'{msg}' at MX:{record}" - }) - logger.info(f'Temporary error: {code} - {msg}') - logger.error('Received multiple temporary errors. Could not finish validation.') - return Response(dictionary={ - 'ok': None, - 'status': 207, - 'body': 'Received multiple temporary errors. Could not finish validation.' - }) + logger.info(f"Valid email address: {address.email}") + return Response( + dictionary={ + "ok": True, + "status": 200, + "body": f"'{msg}' at MX:{record}", + } + ) + logger.info(f"Temporary error: {code} - {msg}") + logger.error("Received multiple temporary errors. Could not finish validation.") + return Response( + dictionary={ + "ok": None, + "status": 207, + "body": "Received multiple temporary errors. Could not finish validation.", + } + ) except (InvalidDomain, NotMailServer, UnresponsiveMailServer) as error: logger.error(error) - return Response(dictionary={ - 'ok': False, - 'status': 422, - 'body': error.__str__() - }) + return Response( + dictionary={"ok": False, "status": 422, "body": error.__str__()} + ) diff --git a/release_notes.rst b/release_notes.rst index 3035135..d982430 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,6 +1,11 @@ Release Notes ============= +v1.0.1 (05/26/2024) +------------------- +- Includes a retry logic for occasional errors while sending emails +- Improved linting and updates to docs and dependencies + v1.0 (10/26/2023) ----------------- - Uses ``pydantic`` for input validations diff --git a/test_runner.py b/test_runner.py index 5584dd8..0e60b2f 100644 --- a/test_runner.py +++ b/test_runner.py @@ -6,16 +6,24 @@ logger = logging.getLogger(__name__) handler = logging.StreamHandler() -handler.setFormatter(logging.Formatter('%(asctime)s - [%(module)s:%(lineno)d] - %(funcName)s - %(message)s')) +handler.setFormatter( + logging.Formatter( + "%(asctime)s - [%(module)s:%(lineno)d] - %(funcName)s - %(message)s" + ) +) logger.addHandler(handler) -if os.getenv('debug'): +if os.getenv("debug"): debug = True logger.setLevel(logging.DEBUG) else: debug = False logger.setLevel(logging.INFO) -logger.info("RUNNING TESTS on version: %s with logger level: %s", gc.version, logging.getLevelName(logger.level)) +logger.info( + "RUNNING TESTS on version: %s with logger level: %s", + gc.version, + logging.getLevelName(logger.level), +) def test_run_read_email(): @@ -26,9 +34,13 @@ def test_run_read_email(): filter2 = gc.Condition.subject(subject="Security Alert") filter3 = gc.Condition.text(text=reader.env.gmail_user) filter4 = gc.Category.not_deleted - response = reader.instantiate(filters=(filter1, filter2, filter3, filter4)) # Apply multiple filters + response = reader.instantiate( + filters=(filter1, filter2, filter3, filter4) + ) # Apply multiple filters assert response.status <= 299, response.body - for each_mail in reader.read_mail(messages=response.body, humanize_datetime=False): # False to get datetime object + for each_mail in reader.read_mail( + messages=response.body, humanize_datetime=False + ): # False to get datetime object logger.debug(each_mail.date_time.date()) logger.debug("[%s] %s" % (each_mail.sender_email, each_mail.sender)) logger.debug("[%s] - %s" % (each_mail.subject, each_mail.body)) @@ -41,8 +53,12 @@ def test_run_send_email_tls(): sender = gc.SendEmail(encryption=gc.Encryption.TLS) auth_status = sender.authenticate assert auth_status.ok, auth_status.body - response = sender.send_email(recipient=sender.env.recipient, sender="GmailConnector Tester", - subject="GmailConnector Test Run - TLS - " + datetime.datetime.now().strftime('%c')) + response = sender.send_email( + recipient=sender.env.recipient, + sender="GmailConnector Tester", + subject="GmailConnector Test Run - TLS - " + + datetime.datetime.now().strftime("%c"), + ) assert response.ok, response.body logger.info("Test successful on send email using TLS") @@ -53,8 +69,12 @@ def test_run_send_email_ssl(): sender = gc.SendEmail(encryption=gc.Encryption.SSL) auth_status = sender.authenticate assert auth_status.ok, auth_status.body - response = sender.send_email(recipient=sender.env.recipient, sender="GmailConnector Tester", - subject="GmailConnector Test Run - SSL - " + datetime.datetime.now().strftime('%c')) + response = sender.send_email( + recipient=sender.env.recipient, + sender="GmailConnector Tester", + subject="GmailConnector Test Run - SSL - " + + datetime.datetime.now().strftime("%c"), + ) assert response.ok, response.body logger.info("Test successful on send email using SSL") @@ -65,8 +85,12 @@ def test_run_send_sms_tls(): sender = gc.SendSMS(encryption=gc.Encryption.TLS) auth_status = sender.authenticate assert auth_status.ok, auth_status.body - response = sender.send_sms(subject="GmailConnector Test Run - TLS", delete_sent=True, - message=datetime.datetime.now().strftime('%c')) + response = sender.send_sms( + subject="GmailConnector Test Run - TLS", + delete_sent=True, + phone="1234567890", + message=datetime.datetime.now().strftime("%c"), + ) assert response.ok, response.body logger.info("Test successful on send sms using TLS") @@ -77,8 +101,12 @@ def test_run_send_sms_ssl(): sender = gc.SendSMS(encryption=gc.Encryption.SSL) auth_status = sender.authenticate assert auth_status.ok, auth_status.body - response = sender.send_sms(subject="GmailConnector Test Run - SSL", delete_sent=True, - message=datetime.datetime.now().strftime('%c')) + response = sender.send_sms( + subject="GmailConnector Test Run - SSL", + delete_sent=True, + phone="1234567890", + message=datetime.datetime.now().strftime("%c"), + ) assert response.ok, response.body logger.info("Test successful on send sms using SSL") @@ -86,7 +114,9 @@ def test_run_send_sms_ssl(): def test_run_validate_email_smtp_off(): """Test run on email validator with SMTP disabled.""" logger.info("Test initiated on email validator with SMTP disabled.") - response = gc.validate_email(gc.EgressConfig().gmail_user, smtp_check=False, debug=debug, logger=logger) + response = gc.validate_email( + gc.EgressConfig().gmail_user, smtp_check=False, debug=debug, logger=logger + ) assert response.ok, response.body logger.info("Test successful on validate email with SMTP enabled.") @@ -94,12 +124,14 @@ def test_run_validate_email_smtp_off(): def test_run_validate_email_smtp_on(): """Test run on email validator with SMTP enabled.""" logger.info("Test initiated on email validator with SMTP enabled.") - response = gc.validate_email(gc.IngressConfig().gmail_user, smtp_check=True, debug=debug, logger=logger) + response = gc.validate_email( + gc.IngressConfig().gmail_user, smtp_check=True, debug=debug, logger=logger + ) assert response.status <= 299, response.body logger.info("Test successful on validate email with SMTP disabled.") -if __name__ == '__main__': +if __name__ == "__main__": test_run_validate_email_smtp_off() test_run_validate_email_smtp_on() test_run_send_email_tls()