home » zplus/clif.git
Author zPlus <zplus@peers.community> 2022-08-03 06:32:22
Committer zPlus <zplus@peers.community> 2022-08-03 06:32:22
Commit cd58ca2 (patch)
Tree 23519b6
Parent(s)

Accept lists subscription via email. Allow +subscribe and +unsubscribe in mailing lists for subscribing to notifications.


commits diff: 2c55b65..cd58ca2
2 files changed, 118 insertions, 28 deletionsdownload


Diffstat
-rwxr-xr-x emails.py 141
-rw-r--r-- templates/mailing_list/mailing_list.html 5

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+113/-28 M   emails.py
index 1f47ce8..032b532
old size: 9K - new size: 11K
@@ -52,61 +52,82 @@ logging.basicConfig(filename='/home/git/clif/emails.log',
52 52 message_raw = sys.stdin.read()
53 53 message = email.message_from_string(message_raw, policy=email.policy.default)
54 54
55 - email_id = message.get('message-id')
56 - email_id_hash = hashlib.sha256(email_id.encode('utf-8')).hexdigest()[:8] # This will be used as thread ID
57 - email_from = email.utils.parseaddr(message.get('from'))
58 - email_to = email.utils.parseaddr(message.get('to'))
59 - email_in_reply_to = message.get('in-reply-to')
60 - email_subject = message.get('subject')
61 - email_body = message.get_body(('plain',)).get_content() # Accept plaintext only!
55 + try:
56 + email_id = message.get('message-id').strip()
57 + except:
58 + logging.error('Refuting email without a Message-ID: {}'.format(email_subject))
59 + exit()
62 60
63 - logging.info('Received email from {} to {} with subject {}'.format(email_from, email_to, email_subject))
61 + email_id_hash = hashlib.sha256(email_id.encode('utf-8')).hexdigest()[:8] # This will be used as thread ID
64 62
65 - if not email_id:
66 - logging.info('Refuting email without a Message-ID: {}'.format(email_subject))
63 + try:
64 + email_from = email.utils.parseaddr(message.get('from'))
65 + assert len(email_from[1]) > 0
66 + except:
67 + logging.error('Refuting email with From header: {}'.format(email_from))
67 68 exit()
68 69
69 - if not email_body:
70 - logging.warning('Refuting email without plaintext body: {}'.format(email_subject))
70 + try:
71 + email_to = email.utils.parseaddr(message.get('to'))
72 + assert len(email_from[1]) > 0
73 + assert email_to[1].endswith('@' + SERVER_DOMAIN)
74 + except:
75 + logging.error('Refuting email with To header: {}'.format(email_to))
71 76 exit()
72 77
73 - if not email_to[1].endswith('@' + SERVER_DOMAIN):
74 - logging.warning('Refuting email with bad recipient domain: {}'.format(email_to))
75 - exit()
78 + email_in_reply_to = message.get('in-reply-to')
79 + if email_in_reply_to:
80 + email_in_reply_to = email_in_reply_to.strip()
81 +
82 + try:
83 + email_subject = message.get('subject').strip()
84 + except:
85 + email_subject = ''
86 +
87 + try:
88 + # Accept plaintext only!
89 + email_body = message.get_body(('plain',)).get_content()
90 + except:
91 + email_body = ''
92 +
93 + logging.info('Received email from {} to {} with subject "{}"'.format(email_from, email_to, email_subject))
76 94
77 95 # Get the repository name. We use email addresses formatted as <repository>@SERVER_DOMAIN
78 96 repository_name = email_to[1].rsplit('@', 1)[0]
97 +
98 + # Is this a request for subscription?
99 + request_subscribe = repository_name.endswith('+subscribe')
100 + request_unsubscribe = repository_name.endswith('+unsubscribe')
101 +
102 + # Remove leading command: from address
103 + if request_subscribe: repository_name = repository_name[:-10]
104 + if request_unsubscribe: repository_name = repository_name[:-12]
105 +
79 106 repository_path = os.path.join(REPOSITORIES_PATH, repository_name + '.mlist.git')
80 107
81 108 if '..' in repository_name:
82 - logging.warning('Refuting email because the repository name contains "..": {}'.format(repository_name))
109 + logging.error('Refuting email because the repository name contains "..": {}'.format(repository_name))
83 110 exit()
84 111
112 + # All repositories should be <username>/<reponame>
85 113 if '/' not in repository_name:
86 - logging.warning('Refuting email because the repository name does not contain a namespace: {}'.format(repository_name))
114 + logging.error('Refuting email because the repository name does not contain a namespace: {}'.format(repository_name))
87 115 exit()
88 116
89 -
90 -
91 -
92 - ###############################################################################
93 - # ADD EMAIL TO USER REPOSITORY
94 - ###############################################################################
95 -
96 117 if not os.path.isdir(repository_path):
97 - logging.warning('Repository path does not exist: {}'.format(repository_path))
118 + logging.error('Repository path does not exist: {}'.format(repository_path))
98 119 exit()
99 120
100 121 try:
101 122 repo = pygit2.Repository(repository_path)
102 123 except:
103 - logging.warning('Not a valid repository: {}'.format(repository_path))
124 + logging.error('Not a valid repository: {}'.format(repository_path))
104 125 exit()
105 126
106 127 try:
107 128 head_tree = repo.revparse_single('HEAD').tree
108 129 except:
109 - logging.warning('Could not find HEAD ref: {}'.format(repository_path))
130 + logging.error('Could not find HEAD ref: {}'.format(repository_path))
110 131 exit()
111 132
112 133 try:
@@ -117,7 +138,71 @@ try:
117 138 subscribers.append(addr)
118 139 except:
119 140 subscribers = []
120 - logging.info('Could not load subscribers file: {}'.format(repository_path))
141 + logging.info('Subscribers file not found in {}'.format(repository_path))
142 +
143 +
144 +
145 +
146 + ###############################################################################
147 + # LISTS SUBSCRIPTION
148 + ###############################################################################
149 +
150 + if request_subscribe \
151 + and email_from[1] in subscribers:
152 + # Already subscribed
153 + exit()
154 +
155 + if request_unsubscribe \
156 + and email_from[1] not in subscribers:
157 + # No address to removed
158 + exit()
159 +
160 + if request_subscribe or request_unsubscribe:
161 + if request_subscribe:
162 + subscribers.append(email_from[1])
163 + commit_message = 'Subscribe'
164 +
165 + if request_unsubscribe:
166 + subscribers = [ address for address in subscribers if address != email_from[1] ]
167 + commit_message = 'Unsubscribe'
168 +
169 + # Add a new BLOB to the git store
170 + oid = repo.create_blob('\n'.join(subscribers).encode('UTF-8'))
171 +
172 + # Add the blob that we've just created to the HEAD tree
173 + head_tree_builder = repo.TreeBuilder(head_tree)
174 + head_tree_builder.insert('subscribers', oid, pygit2.GIT_FILEMODE_BLOB)
175 + head_tree_oid = head_tree_builder.write()
176 +
177 + repo.create_commit(
178 + repo.head.name, # reference name
179 + pygit2.Signature('CLIF', '-'), # author
180 + pygit2.Signature('CLIF', '-'), # committer
181 + commit_message, # message
182 + head_tree_oid, # tree of this commit
183 + [ repo.head.target ] # parents commit
184 + )
185 +
186 + exit()
187 +
188 +
189 +
190 +
191 + ###############################################################################
192 + # ADD EMAIL TO USER REPOSITORY
193 + ###############################################################################
194 +
195 + if len(email_subject) == 0:
196 + logging.info('Refuting email with no subject: {}'.format(email_id))
197 + exit()
198 +
199 + if not email_body:
200 + logging.warning('Refuting email without plaintext body: {}'.format(email_subject))
201 + exit()
202 +
203 + if len(email_body.strip()) == 0:
204 + logging.info('Refuting email with empty body: {}'.format(email_id))
205 + exit()
121 206
122 207 logging.debug('Accepting email from {} to {} with subject {}'.format(email_from, email_to, email_subject))
123 208

+5/-0 M   templates/mailing_list/mailing_list.html
index 8de2765..0d7b47b
old size: 244B - new size: 446B
@@ -3,6 +3,11 @@
3 3 {% block path %}
4 4 <a href="/">home</a> »
5 5 <a href="{{ url('threads', repository=repository[:-10]) }}">{{ list_address }}</a>
6 + (
7 + <a href="mailto:{{ repository[:-10] }}+subscribe@{{ instance_domain }}">Subscribe</a>
8 + -
9 + <a href="mailto:{{ repository[:-10] }}+unsubscribe@{{ instance_domain }}">Unsubscribe</a>
10 + )
6 11 {% endblock %}
7 12
8 13 {% block context %}