summaryrefslogtreecommitdiff
path: root/patch-save.py
blob: 8508d2264c4f3d8587211031364ae6cb66bc41bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2022 Laurent Pinchart
#
# Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
#
# Patch save script for mutt that handles mailman From header mangling.
#
# To install, copy the script to ~/.mutt/, and add the following lines to your
# .muttrc to bind to ctrl-h.
#
# set pipe_split=yes
# macro index \Ch "<enter-command>unset wait_key<enter>
#   <enter-command>set pipe_decode<enter>
#   <tag-prefix><pipe-entry>~/.mutt/patch-save.py<enter>
#   <enter-command>unset pipe_decode<enter>
#   <enter-command>set wait_key<enter>" "output git patches"
#

import email
import email.parser
import email.policy
import pathlib
import re
import sys


def sanitize(s):
    out = ''

    for c in s:
        if c in '[]':
            continue

        if c in "'":
            c = '.'
        elif c in '/:':
            c = '-'
        elif c in '*()" \t\n\r':
            c = '_'

        out += c

    return out


def main(argv):

    # Parse the message from stdin as binary, as we can't assume any particular
    # encoding.
    parser = email.parser.BytesParser(policy=email.policy.default)
    msg = parser.parse(sys.stdin.buffer)

    # Handle mailman message mangling used to work around DMARC rejection rules:
    #
    # - mailman may encapsulate original messages in a message/rfc822 part.
    #   That's an easy case, simply extract the encapsulated message. Any
    #   additional mangling of the outer message (such as From header mangling)
    #   can be ignored.
    #
    # - mailman may mangle the From header. It then formats From as "Name via
    #   mailing-list <mailing-list@domain>" and stores the original sender in
    #   the Reply-to header. Restore the original author in the From header to
    #   please git-am.

    if msg.get_content_type() == 'message/rfc822':
        msg = next(msg.iter_parts())
    else:
        from_header = msg.get('From')
        reply_to = msg.get('Reply-to')
        if reply_to and ' via ' in from_header:
            msg.replace_header('From', reply_to)

    subject = msg.get('Subject')
    if not subject:
        return 1

    # The subject can be an str instance or an email.header.Header instance.
    # Convert it to str in all cases.
    subject = str(subject)

    # Strip everything before the [PATCH] or [RESEND] tag.
    match = re.search(r'\[(PATCH|RESEND) [^]]*\].*', subject)
    if match:
        subject = match.group(0)

    # Sanitize the subject.
    subject = sanitize(subject)

    subject = subject.replace('..', '.')
    subject = subject.replace('-_', '_')
    subject = subject.replace('__', '_')

    subject = subject.strip()
    subject = subject.rstrip('.')

    file_name = pathlib.Path.home() / '.maildir' / 'patches' / (subject + '.patch')
    open(file_name, 'wb').write(bytes(msg))

    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))