mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-05 05:11:35 +00:00
Initial commit of a new experimental modular GAM.
This commit is contained in:
193
src/gam/util/email.py
Normal file
193
src/gam/util/email.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""GAM email utilities.
|
||||
|
||||
Extracted from gam/__init__.py. Provides email attachment handling
|
||||
and email sending via Gmail API or SMTP.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import smtplib
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.audio import MIMEAudio
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glgapi as GAPI
|
||||
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
|
||||
# Add attachements to an email message
|
||||
def _addAttachmentsToMessage(message, attachments):
|
||||
gam = _getMain()
|
||||
for attachment in attachments:
|
||||
try:
|
||||
attachFilename = gam.setFilePath(attachment[0], GC.INPUT_DIR)
|
||||
attachContentType, attachEncoding = mimetypes.guess_type(attachFilename)
|
||||
if attachContentType is None or attachEncoding is not None:
|
||||
attachContentType = 'application/octet-stream'
|
||||
main_type, sub_type = attachContentType.split('/', 1)
|
||||
if main_type == 'text':
|
||||
msg = MIMEText(gam.readFile(attachFilename, 'r', attachment[1]), _subtype=sub_type, _charset=gam.UTF8)
|
||||
elif main_type == 'image':
|
||||
msg = MIMEImage(gam.readFile(attachFilename, 'rb'), _subtype=sub_type)
|
||||
elif main_type == 'audio':
|
||||
msg = MIMEAudio(gam.readFile(attachFilename, 'rb'), _subtype=sub_type)
|
||||
elif main_type == 'application':
|
||||
msg = MIMEApplication(gam.readFile(attachFilename, 'rb'), _subtype=sub_type)
|
||||
else:
|
||||
msg = MIMEBase(main_type, sub_type)
|
||||
msg.set_payload(gam.readFile(attachFilename, 'rb'))
|
||||
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(attachFilename))
|
||||
message.attach(msg)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
gam.usageErrorExit(f'{attachFilename}: {str(e)}')
|
||||
|
||||
# Add embedded images to an email message
|
||||
def _addEmbeddedImagesToMessage(message, embeddedImages):
|
||||
gam = _getMain()
|
||||
for embeddedImage in embeddedImages:
|
||||
try:
|
||||
imageFilename = gam.setFilePath(embeddedImage[0], GC.INPUT_DIR)
|
||||
imageContentType, imageEncoding = mimetypes.guess_type(imageFilename)
|
||||
if imageContentType is None or imageEncoding is not None:
|
||||
imageContentType = 'application/octet-stream'
|
||||
main_type, sub_type = imageContentType.split('/', 1)
|
||||
if main_type == 'image':
|
||||
msg = MIMEImage(gam.readFile(imageFilename, 'rb'), _subtype=sub_type)
|
||||
else:
|
||||
msg = MIMEBase(main_type, sub_type)
|
||||
msg.set_payload(gam.readFile(imageFilename, 'rb'))
|
||||
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(imageFilename))
|
||||
msg.add_header('Content-ID', f'<{embeddedImage[1]}>')
|
||||
message.attach(msg)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
gam.usageErrorExit(f'{imageFilename}: {str(e)}')
|
||||
|
||||
# Send an email
|
||||
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
|
||||
html=False, charset=None, attachments=None, embeddedImages=None,
|
||||
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None,
|
||||
action=None):
|
||||
gam = _getMain()
|
||||
Act = gam.Act
|
||||
Ent = gam.Ent
|
||||
if charset is None:
|
||||
charset = gam.UTF8
|
||||
if action is None:
|
||||
action = Act.SENDEMAIL
|
||||
|
||||
def checkResult(entityType, recipients):
|
||||
if not recipients:
|
||||
return
|
||||
toSent = set(recipients.split(','))
|
||||
toFailed = {}
|
||||
for addr, err in result.items():
|
||||
if addr in toSent:
|
||||
toSent.remove(addr)
|
||||
toFailed[addr] = f'{err[0]}: {err[1]}'
|
||||
if toSent:
|
||||
gam.entityActionPerformed([entityType, ','.join(toSent), Ent.MESSAGE, msgSubject], i, count)
|
||||
for addr, errMsg in toFailed.items():
|
||||
gam.entityActionFailedWarning([entityType, addr, Ent.MESSAGE, msgSubject], errMsg, i, count)
|
||||
|
||||
def cleanAddr(emailAddr):
|
||||
match = gam.NAME_EMAIL_ADDRESS_PATTERN.match(emailAddr)
|
||||
if match:
|
||||
emailName = match.group(1)
|
||||
emailAddr = gam.normalizeEmailAddressOrUID(match.group(2), noUid=True, noLower=True)
|
||||
return (f'{emailName} <{emailAddr}>', emailAddr)
|
||||
emailAddr = gam.normalizeEmailAddressOrUID(emailAddr, noUid=True, noLower=True)
|
||||
return (emailAddr, emailAddr)
|
||||
|
||||
if msgFrom is None:
|
||||
msgFrom = gam._getAdminEmail()
|
||||
# Force ASCII for RFC compliance
|
||||
# xmlcharref seems to work to display at least
|
||||
# some unicode in HTML body and is ignored in
|
||||
# plain text body.
|
||||
# msgBody = msgBody.encode('ascii', 'xmlcharrefreplace').decode(gam.UTF8)
|
||||
if not attachments and not embeddedImages:
|
||||
message = MIMEText(msgBody, ['plain', 'html'][html], charset)
|
||||
else:
|
||||
message = MIMEMultipart()
|
||||
msg = MIMEText(msgBody, ['plain', 'html'][html], charset)
|
||||
message.attach(msg)
|
||||
if attachments:
|
||||
_addAttachmentsToMessage(message, attachments)
|
||||
if embeddedImages:
|
||||
_addEmbeddedImagesToMessage(message, embeddedImages)
|
||||
message['Subject'] = msgSubject
|
||||
message['From'], msgFromAddr = cleanAddr(msgFrom)
|
||||
if msgReplyTo is not None:
|
||||
message['Reply-To'], _ = cleanAddr(msgReplyTo)
|
||||
if ccRecipients:
|
||||
message['Cc'] = ccRecipients.lower()
|
||||
if bccRecipients:
|
||||
message['Bcc'] = bccRecipients.lower()
|
||||
if msgHeaders:
|
||||
for header, value in msgHeaders.items():
|
||||
if header not in {'Subject', 'From', 'To', 'Reply-To', 'Cc', 'Bcc'}:
|
||||
message[header] = value
|
||||
if mailBox is None:
|
||||
mailBox = msgFromAddr
|
||||
_, mailBoxAddr = cleanAddr(mailBox)
|
||||
parentAction = Act.Get()
|
||||
Act.Set(action)
|
||||
if not GC.Values[GC.SMTP_HOST]:
|
||||
if not clientAccess:
|
||||
userId, gmail = gam.buildGAPIServiceObject(API.GMAIL, mailBoxAddr)
|
||||
if not gmail:
|
||||
Act.Set(parentAction)
|
||||
return
|
||||
else:
|
||||
userId = mailBoxAddr
|
||||
gmail = gam.buildGAPIObject(API.GMAIL)
|
||||
message['To'] = msgTo if msgTo else userId
|
||||
body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}
|
||||
if threadId is not None:
|
||||
body['threadId'] = threadId
|
||||
try:
|
||||
result = gam.callGAPI(gmail.users().messages(), 'send',
|
||||
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
|
||||
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
||||
userId=userId, body=body, fields='id')
|
||||
gam.entityActionPerformedMessage([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], f"{result['id']}", i, count)
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
|
||||
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied) as e:
|
||||
gam.entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
|
||||
else:
|
||||
message['To'] = msgTo if msgTo else mailBoxAddr
|
||||
server = None
|
||||
try:
|
||||
server = smtplib.SMTP(GC.Values[GC.SMTP_HOST], 587, GC.Values[GC.SMTP_FQDN])
|
||||
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
||||
server.set_debuglevel(1)
|
||||
server.starttls(context=ssl.create_default_context(cafile=GC.Values[GC.CACERTS_PEM]))
|
||||
if GC.Values[GC.SMTP_USERNAME] and GC.Values[GC.SMTP_PASSWORD]:
|
||||
if isinstance(GC.Values[GC.SMTP_PASSWORD], bytes):
|
||||
server.login(GC.Values[GC.SMTP_USERNAME], base64.b64decode(GC.Values[GC.SMTP_PASSWORD]).decode(gam.UTF8))
|
||||
else:
|
||||
server.login(GC.Values[GC.SMTP_USERNAME], GC.Values[GC.SMTP_PASSWORD])
|
||||
result = server.send_message(message)
|
||||
checkResult(Ent.RECIPIENT, message['To'])
|
||||
checkResult(Ent.RECIPIENT_CC, ccRecipients)
|
||||
checkResult(Ent.RECIPIENT_BCC, bccRecipients)
|
||||
except smtplib.SMTPException as e:
|
||||
gam.entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
|
||||
if server:
|
||||
try:
|
||||
server.quit()
|
||||
except Exception:
|
||||
pass
|
||||
Act.Set(parentAction)
|
||||
Reference in New Issue
Block a user