Skip to content

Commit

Permalink
Add a retry logic for occasional errors while sending emails
Browse files Browse the repository at this point in the history
Update docstrings and improve project's overall linting
  • Loading branch information
dormant-user committed Apr 3, 2024
1 parent 7a4a35e commit 4412d2a
Show file tree
Hide file tree
Showing 16 changed files with 126 additions and 66 deletions.
51 changes: 32 additions & 19 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
fail_fast: true
exclude: ^docs/
repos:
-
repo: https://github.com/PyCQA/flake8
rev: '6.1.0'
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
-
id: flake8
additional_dependencies:
- flake8-docstrings
- flake8-sfs
args: [--max-line-length=120, --extend-ignore=SFS3 D107 SFS301 D100 D104 D401 SFS101]
- 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'
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
-
id: isort
- id: isort

-
repo: local
- repo: local
hooks:
-
id: build_docs
name: build_html
- id: runbook
name: runbook
entry: /bin/bash gen_docs.sh
language: system
pass_filenames: false
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ recursive-include gmailconnector *
recursive-exclude venv *
recursive-exclude docs *
recursive-exclude doc_generator *
exclude gen_docs.sh .gitignore .pre-commit-config.yaml
exclude gen_docs.sh .gitignore .pre-commit-config.yaml
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# Gmail Connector
Python module to send SMS, emails and read emails in any folder.

> As of May 30, 2022, Google no longer supports third party applications accessing Google accounts only using username
> As of May 30, 2022, Google no longer supports third party applications accessing Google accounts only using username
> and password (which was originally available through [lesssecureapps](https://myaccount.google.com/lesssecureapps))
> <br>
> An alternate approach is to generate [apppasswords](https://myaccount.google.com/apppasswords) instead.<br>
Expand All @@ -41,7 +41,6 @@ To load a custom `.env` file, set the filename as the env var `env_file` before
```python
import os
os.environ['env_file'] = 'custom' # to load a custom .env file
import gmailconnector as gc
```
To avoid using env variables, arguments can be loaded during object instantiation.
```python
Expand Down
1 change: 0 additions & 1 deletion docs/README.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ <h2>Env Vars<a class="headerlink" href="#env-vars" title="Permalink to this head
<summary><strong>Env variable customization</strong></summary><p>To load a custom <code class="docutils literal notranslate"><span class="pre">.env</span></code> file, set the filename as the env var <code class="docutils literal notranslate"><span class="pre">env_file</span></code> before importing <code class="docutils literal notranslate"><span class="pre">gmailconnector</span></code></p>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">os</span>
<span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s1">&#39;env_file&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;custom&#39;</span> <span class="c1"># to load a custom .env file</span>
<span class="kn">import</span> <span class="nn">gmailconnector</span> <span class="k">as</span> <span class="nn">gc</span>
</pre></div>
</div>
<p>To avoid using env variables, arguments can be loaded during object instantiation.</p>
Expand Down
3 changes: 1 addition & 2 deletions docs/_sources/README.md.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# Gmail Connector
Python module to send SMS, emails and read emails in any folder.

> As of May 30, 2022, Google no longer supports third party applications accessing Google accounts only using username
> As of May 30, 2022, Google no longer supports third party applications accessing Google accounts only using username
> and password (which was originally available through [lesssecureapps](https://myaccount.google.com/lesssecureapps))
> <br>
> An alternate approach is to generate [apppasswords](https://myaccount.google.com/apppasswords) instead.<br>
Expand All @@ -41,7 +41,6 @@ To load a custom `.env` file, set the filename as the env var `env_file` before
```python
import os
os.environ['env_file'] = 'custom' # to load a custom .env file
import gmailconnector as gc
```
To avoid using env variables, arguments can be loaded during object instantiation.
```python
Expand Down
31 changes: 19 additions & 12 deletions docs/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/searchindex.js

Large diffs are not rendered by default.

Empty file modified gen_docs.sh
100644 → 100755
Empty file.
4 changes: 2 additions & 2 deletions gmailconnector/lib/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
idna==3.*
pytz==2023.*
dnspython==2.4.2
idna==3.*
pydantic[email]==2.4.*
pydantic_settings==2.0.*
pytz==2023.*
13 changes: 9 additions & 4 deletions gmailconnector/read_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,17 @@ def __init__(self, **kwargs: 'Unpack[IngressConfig]'):
self.env = IngressConfig(**kwargs)
self.create_ssl_connection(gmail_host=self.env.gmail_host, timeout=self.env.timeout)

def create_ssl_connection(self, gmail_host: str, timeout: Union[int, float]) -> None:
def create_ssl_connection(self,
gmail_host: str,
timeout: Union[int, float]) -> None:
"""Creates an SSL connection to gmail's SSL server."""
try:
self.mail = imaplib.IMAP4_SSL(host=gmail_host, port=993, timeout=timeout)
except socket.error as error:
self.error = error.__str__()

@property
def authenticate(self):
def authenticate(self) -> Response:
"""Initiates authentication.
Returns:
Expand Down Expand Up @@ -133,7 +135,9 @@ def instantiate(self,
'count': num
})

def get_info(self, response_part: tuple, dt_flag: bool) -> Email:
def get_info(self,
response_part: tuple,
dt_flag: bool) -> Email:
"""Extracts sender, subject, body and time received from response part.
Args:
Expand Down Expand Up @@ -204,7 +208,8 @@ def get_info(self, response_part: tuple, dt_flag: bool) -> Email:
return Email(dictionary=dict(sender=from_[0], sender_email=from_[1].rstrip('>'),
subject=sub, date_time=receive, body=body))

def read_mail(self, messages: list or str, humanize_datetime: bool = False) -> Generator[Email]:
def read_mail(self, messages: Union[list, str],
humanize_datetime: bool = False) -> Generator[Email]:
"""Yield emails matching the filters' criteria.
Args:
Expand Down
53 changes: 38 additions & 15 deletions gmailconnector/send_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Dict, Union
from typing import Dict, List, Union

from typing_extensions import Unpack

Expand All @@ -13,7 +13,7 @@
from .validator.address import EmailAddress


def validate_email(address: Union[str, list]) -> Union[str, list]:
def validate_email(address: Union[str, List[str]]) -> Union[str, List[str]]:
"""Validates email addresses and returns them as is."""
if isinstance(address, str):
return EmailAddress(address).email
Expand Down Expand Up @@ -54,7 +54,9 @@ 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)
Expand Down Expand Up @@ -102,13 +104,21 @@ def __del__(self):
if self.server:
self.server.close()

def multipart_message(self, subject: str, recipient: str or list, sender: str, body: str, html_body: str,
attachments: list, filenames: list, cc: str or list) -> 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:
recipient: Email address of the recipient to whom the email has to be sent.
subject: Subject line of the email.
recipient: Email address of the recipient to whom the email has to be sent.
sender: Name of the sender.
body: Body of the email. Defaults to ``None``.
html_body: Body of the email. Defaults to ``None``.
attachments: Names of the files that has to be attached.
Expand Down Expand Up @@ -166,11 +176,17 @@ def multipart_message(self, subject: str, recipient: str or list, sender: str, b

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,
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,
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.
Expand Down Expand Up @@ -221,11 +237,18 @@ def send_email(self, subject: str, recipient: Union[str, list],
recipients.append(cc) if isinstance(cc, str) else recipients.extend(cc)
if bcc:
recipients.append(bcc) if isinstance(bcc, str) else recipients.extend(bcc)
self.server.sendmail(
from_addr=sender,
to_addrs=recipients,
msg=msg.as_string()
)
for i in range(3):
try:
self.server.sendmail(
from_addr=sender,
to_addrs=recipients,
msg=msg.as_string()
)
break
except smtplib.SMTPServerDisconnected as err:
if i == 2:
raise err
continue
if unattached:
return Response(dictionary={
'ok': True,
Expand Down
8 changes: 6 additions & 2 deletions gmailconnector/send_sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ def __init__(self, **kwargs: 'Unpack[EgressConfig]'):
else:
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)
Expand Down
3 changes: 2 additions & 1 deletion gmailconnector/sms_deleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def create_ssl_connection(self) -> None:
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:
Expand Down
8 changes: 7 additions & 1 deletion gmailconnector/validator/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ class EmailAddress:
"""

def __init__(self, address: str):
def __init__(self,
address: str):
"""Converts address into IDNA (Internationalized Domain Name) format.
Args:
address: Email address as a string.
"""
self._address = address
try:
self._user, self._domain = self._address.rsplit('@', 1)
Expand Down
8 changes: 6 additions & 2 deletions gmailconnector/validator/validate_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
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:
Expand Down

0 comments on commit 4412d2a

Please sign in to comment.