Skip to content

Commit 1cb98ef

Browse files
committed
feature: base64 image convert to attachments
fix: removed <> fix: removing non alphanumeric characters fix: use inline type for disposition
1 parent 83c65b5 commit 1cb98ef

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

django_email_verification/confirm.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .errors import InvalidUserModel, NotAllFieldCompiled
1515
from .token_utils import default_token_generator
16+
from .utils import convert_base64_images
1617

1718
logger = logging.getLogger('django_email_verification')
1819
DJANGO_EMAIL_VERIFICATION_MORE_VIEWS_ERROR = 'ERROR: more than one verify view found'
@@ -81,13 +82,20 @@ def has_decorator(k):
8182
if not validators.url(context['link']):
8283
logger.warning(f'{DJANGO_EMAIL_VERIFICATION_MALFORMED_URL} - {context["link"]}')
8384

85+
do_convert_base64_images = _get_validated_field(f'EMAIL_CONVERT_BASE64_IMAGES', default=False, use_default=True, default_type=bool)
86+
87+
attachments = []
88+
if do_convert_base64_images:
89+
# Look for inline base64 images and converts them to attachments for email providers that require them i.e. Gmail
90+
mail_html, attachments = convert_base64_images(mail_html, attachments)
91+
8492
subject = Template(subject).render(Context(context))
8593

8694
text = render_to_string(mail_plain, context)
8795

8896
html = render_to_string(mail_html, context)
8997

90-
msg = EmailMultiAlternatives(subject, text, sender, [user.email])
98+
msg = EmailMultiAlternatives(subject, text, sender, [user.email], attachments=attachments)
9199

92100
if debug:
93101
msg.extra_headers['LINK'] = context['link']

django_email_verification/utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
import base64
3+
import hashlib
4+
import random
5+
import re
6+
import string
7+
8+
from email.mime.image import MIMEImage
9+
10+
def random_string(length, case="lowercase"):
11+
return "".join(random.choices(getattr(string, f"ascii_{case}") + string.digits, k=length))
12+
13+
def convert_base64_images(body, attachments):
14+
15+
def repl(match):
16+
# Capture subtype in case MIMEImage's use of imghdr.what bugs out in guesesing the file type
17+
subtype = match.group("subtype")
18+
key = hashlib.md5(base64.b64decode(match.group("data"))).hexdigest().replace("-", "")
19+
if key not in base64_images:
20+
base64_images[key] = {
21+
"data": match.group("data"),
22+
"subtype": subtype,
23+
}
24+
return ' src="cid:image-%s"' % key
25+
26+
# Compile pattern for base64 inline images
27+
RE_BASE64_SRC = re.compile(
28+
r' src="data:image/(?P<subtype>gif|png|jpeg|bmp|webp)(?:;charset=utf-8)?;base64,(?P<data>[A-Za-z0-9|+ /]+={0,2})"',
29+
re.MULTILINE)
30+
31+
base64_images = {}
32+
33+
# Replace and add base64 data to base64_images via repl
34+
body = re.sub(RE_BASE64_SRC, repl, body)
35+
for key, image_data in base64_images.items():
36+
try:
37+
image = MIMEImage(base64.b64decode(image_data["data"]))
38+
except TypeError:
39+
# Check for subtype if checking fails
40+
if image_data["subtype"]:
41+
image = MIMEImage(
42+
base64.b64decode(image_data["data"]),
43+
_subtype=image_data["subtype"]
44+
)
45+
else:
46+
raise
47+
image.add_header('Content-ID', '<image-%s>' % key)
48+
image.add_header('Content-Disposition', "inline; filename=%s" % f'image_{random_string(length=6)}')
49+
attachments.append(image)
50+
51+
return body, attachments

0 commit comments

Comments
 (0)