blob: f8f56aa794470cc8a8e8b9ce7cfe8a0137012cc7 [file] [log] [blame]
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL2.0
# Copyright Thomas Gleixner <tglx@linutronix.de>
#
from datetime import timedelta, datetime, timezone
from email.message import EmailMessage
from email.utils import make_msgid
from email.utils import formatdate
from email.header import Header, decode_header
from email.mime.text import MIMEText
from email import message_from_string
import email.policy
import mailbox
import smtplib
import pygit2
import email
import time
import re
import os
def build_raw(addr):
addr = addr.split('>')[0]
try:
return addr.split('<')[1]
except:
return addr
re_fromchars = re.compile('[^a-zA-Z0-9 ]')
re_compress_space = re.compile('\s+')
def clean_header(default, fallback):
default = re_compress_space.sub(' ', default).strip()
try:
return default.encode('ascii').decode()
except:
try:
return default.encode('UTF-8').decode()
except:
if fallback:
return re_compress_space.sub(' ', fallback).strip()
else:
return default
def quote_name(name):
name = name.strip()
if re_fromchars.search(name):
name = '"%s"' %name.replace('"', '')
return name
def clean_from(tip, default, fallback):
default = tip + ' ' + default
if fallback:
fallback = tip + ' ' + fallback
res = clean_header(default, fallback)
return quote_name(res)
def clean_cc(default, fallback, utf8=False):
if default.find('>') > 0:
default = default.split('>')[0] + '>'
try:
name, addr = default.split('<', 1)
name = quote_name(name.strip())
try:
name = name.encode('ascii').decode()
except:
if not utf8:
return fallback
name = name.encode('UTF-8').decode()
return name + ' <' + addr
except:
return fallback
class mailer(object):
cctags = [
"Reported-and-tested-by",
"Reported-by",
"Suggested-by",
"Originally-from",
"Originally-by",
"Signed-off-by",
"Tested-by",
"Reviewed-by",
"Acked-by",
"Cc",
]
def __init__(self, args, logger):
self.args = args
self.log = logger
if args.mbox:
self.mbox = os.path.abspath(args.mbox)
if args.test:
self.forceccs = []
self.optccs = {}
else:
self.forceccs = vars(args).get('forceccs', [])
self.optccs = vars(args).get('optccs', {})
def send_mail(self, repo, branch, sha1):
commit = repo.repo[sha1]
subj = commit.message.split('\n')[0].strip()
ccs = {}
refid = None
for l in commit.message.split('\n'):
try:
tag, rest = l.strip().split(':', 1)
tag = tag.strip()
rest = rest.strip()
if tag in self.cctags and not self.args.test:
if rest.find('@') < 0:
continue
if rest.find('>') >= 0 and rest.find('<') >= 0:
mail = rest.rsplit('>', 1)[0] + '>'
else:
mail = rest
raw = build_raw(mail)
# Don't try the UTF8 header mess
# google mail is unhappy with that
addr = clean_cc(mail, raw)
if raw not in ccs:
ccs[raw] = addr
elif tag == 'Link':
try:
mid = rest.rsplit('/', 1)[1]
if mid.find('@') > 0 and not refid:
refid = mid
except:
pass
except:
pass
for cc in self.forceccs:
raw = build_raw(cc)
if raw not in ccs:
ccs[raw] = cc
for cc, branches in self.optccs.items():
if branch in branches:
raw = build_raw(cc)
if raw not in ccs:
ccs[raw] = cc
body = ''
if self.args.test:
body += '\n-------------------- TEST ---------------------\n\n'
body += 'The following commit has been merged into the %s branch of tip:\n\n' %branch
body += 'Commit-ID: %s\n' %sha1
body += 'Gitweb: https://git.kernel.org/tip/%s\n' %sha1
body += 'Author: %s <%s>\n' %(commit.author.name, commit.author.email)
td = timedelta(minutes = commit.author.offset)
tz = timezone(td)
dt = datetime.fromtimestamp(commit.author.time, tz)
tf = dt.strftime('%a, %d %b %Y %H:%M:%S %Z').replace('UTC', '')
body += 'AuthorDate: %s\n' %tf
body += 'Committer: %s <%s>\n' %(commit.committer.name, commit.committer.email)
td = timedelta(minutes = commit.committer.offset)
tz = timezone(td)
dt = datetime.fromtimestamp(commit.committer.time, tz)
tf = dt.strftime('%a, %d %b %Y %H:%M:%S %Z').replace('UTC', '')
body += 'CommitterDate: %s\n' %tf
body += '\n'
body += commit.message
body += '---\n'
tree = commit.tree
ptre = commit.parents[0].tree
diff = ptre.diff_to_tree(tree)
body += diff.stats.format(format=pygit2.GIT_DIFF_STATS_FULL |
pygit2.GIT_DIFF_STATS_INCLUDE_SUMMARY,
width=70)
body += '\n'
body += diff.patch
body = body.encode('UTF-8').decode()
msg = EmailMessage()
msg['Return-path'] ='tip-bot2@linutronix.de'
msg['Date'] = '%s' %formatdate()
name = clean_from('tip-bot2 for', commit.author.name, commit.author.email.split('@')[0])
mfrom = '%s <tip-bot2@linutronix.de>' %name
msg['From'] = mfrom
msg['Sender'] = 'tip-bot2@linutronix.de'
msg.set_unixfrom('From tip-bot2 ' + time.ctime(time.time()))
if not self.args.test:
msg['Reply-to'] = 'linux-kernel@vger.kernel.org'
msg['To'] = 'linux-tip-commits@vger.kernel.org'
else:
msg['Reply-to'] = 'tglx@linutronix.de'
msg['To'] = 'Thomas Gleixner <tglx@linutronix.de>'
subj = clean_header(subj, None)
subj = '[tip: %s] %s' %(branch, subj)
msg['Subject'] = subj
if len(ccs) > 0:
rcpt = ''
for k, addr in ccs.items():
rcpt += '%s, ' %addr
msg['Cc'] = rcpt.rstrip(', ')
if refid:
msg['In-Reply-To'] = '<%s>' %refid
msg['References'] ='<%s>' %refid
if not msg.get('MIME-Version'):
msg['MIME-Version'] = '1.0'
msg['Message-ID'] = '%s' %make_msgid('tip-bot2')
msg['X-Mailer'] = 'tip-git-log-daemon'
msg['Robot-ID'] = '<tip-bot2.linutronix.de>'
msg['Robot-Unsubscribe'] = 'Contact <mailto:tglx@linutronix.de> to get blacklisted from these emails'
msg['Content-Type'] = 'text/plain'
msg.set_param('charset', 'utf-8', header='Content-Type')
msg['Content-Transfer-Encoding'] = '8bit'
msg['Content-Disposition'] = 'inline'
msg['Precedence'] = 'bulk'
msg.set_content(body)
if self.args.mbox:
mbox = mailbox.mbox(self.mbox, create=True)
try:
mbox.add(msg)
except:
pol = email.policy.EmailPolicy(utf8=True)
mbmsg = EmailMessage(pol)
for k in msg:
if k not in mbmsg:
mbmsg[k] = msg[k]
mbmsg.set_content(msg.get_content())
mbox.add(mbmsg)
mbox.close()
elif not self.args.smtp:
print(msg.as_string())
if self.args.smtp:
to = msg['To']
server = smtplib.SMTP('localhost')
server.ehlo()
server.send_message(msg)
server.quit()