From 1cb98efe176aff176b9d1cbe61a4594359db44c2 Mon Sep 17 00:00:00 2001 From: John Rei Enriquez Date: Sun, 4 Feb 2024 22:11:18 +0800 Subject: [PATCH] feature: base64 image convert to attachments fix: removed <> fix: removing non alphanumeric characters fix: use inline type for disposition --- django_email_verification/confirm.py | 10 +++++- django_email_verification/utils.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 django_email_verification/utils.py diff --git a/django_email_verification/confirm.py b/django_email_verification/confirm.py index 05cea0f..b70981b 100644 --- a/django_email_verification/confirm.py +++ b/django_email_verification/confirm.py @@ -13,6 +13,7 @@ from .errors import InvalidUserModel, NotAllFieldCompiled from .token_utils import default_token_generator +from .utils import convert_base64_images logger = logging.getLogger('django_email_verification') DJANGO_EMAIL_VERIFICATION_MORE_VIEWS_ERROR = 'ERROR: more than one verify view found' @@ -81,13 +82,20 @@ def has_decorator(k): if not validators.url(context['link']): logger.warning(f'{DJANGO_EMAIL_VERIFICATION_MALFORMED_URL} - {context["link"]}') + do_convert_base64_images = _get_validated_field(f'EMAIL_CONVERT_BASE64_IMAGES', default=False, use_default=True, default_type=bool) + + attachments = [] + if do_convert_base64_images: + # Look for inline base64 images and converts them to attachments for email providers that require them i.e. Gmail + mail_html, attachments = convert_base64_images(mail_html, attachments) + subject = Template(subject).render(Context(context)) text = render_to_string(mail_plain, context) html = render_to_string(mail_html, context) - msg = EmailMultiAlternatives(subject, text, sender, [user.email]) + msg = EmailMultiAlternatives(subject, text, sender, [user.email], attachments=attachments) if debug: msg.extra_headers['LINK'] = context['link'] diff --git a/django_email_verification/utils.py b/django_email_verification/utils.py new file mode 100644 index 0000000..84e7c1b --- /dev/null +++ b/django_email_verification/utils.py @@ -0,0 +1,51 @@ + +import base64 +import hashlib +import random +import re +import string + +from email.mime.image import MIMEImage + +def random_string(length, case="lowercase"): + return "".join(random.choices(getattr(string, f"ascii_{case}") + string.digits, k=length)) + +def convert_base64_images(body, attachments): + + def repl(match): + # Capture subtype in case MIMEImage's use of imghdr.what bugs out in guesesing the file type + subtype = match.group("subtype") + key = hashlib.md5(base64.b64decode(match.group("data"))).hexdigest().replace("-", "") + if key not in base64_images: + base64_images[key] = { + "data": match.group("data"), + "subtype": subtype, + } + return ' src="cid:image-%s"' % key + + # Compile pattern for base64 inline images + RE_BASE64_SRC = re.compile( + r' src="data:image/(?Pgif|png|jpeg|bmp|webp)(?:;charset=utf-8)?;base64,(?P[A-Za-z0-9|+ /]+={0,2})"', + re.MULTILINE) + + base64_images = {} + + # Replace and add base64 data to base64_images via repl + body = re.sub(RE_BASE64_SRC, repl, body) + for key, image_data in base64_images.items(): + try: + image = MIMEImage(base64.b64decode(image_data["data"])) + except TypeError: + # Check for subtype if checking fails + if image_data["subtype"]: + image = MIMEImage( + base64.b64decode(image_data["data"]), + _subtype=image_data["subtype"] + ) + else: + raise + image.add_header('Content-ID', '' % key) + image.add_header('Content-Disposition', "inline; filename=%s" % f'image_{random_string(length=6)}') + attachments.append(image) + + return body, attachments