This repository was archived by the owner on Mar 11, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpreposter.us.py
executable file
·390 lines (318 loc) · 16.7 KB
/
preposter.us.py
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#!/usr/bin/python
import imaplib
import email
import os
import hashlib
import smtplib
import sys
import mimetypes
import unicodedata
import re
import ConfigParser
import shutil
import traceback
import humanhash
import json
import xml.etree.ElementTree as ET
#import dateutil
from email.mime.text import MIMEText
# load config
config = ConfigParser.RawConfigParser()
config.read('preposter.us.cfg')
IMAP_SERVER = config.get('mailserver', 'imap_server')
SMTP_SERVER = config.get('mailserver', 'smtp_server')
SMTP_PORT = config.get('mailserver', 'smtp_port')
EMAIL_ADDRESS = config.get('mailserver', 'email_address')
EMAIL_PASSWORD = config.get('mailserver', 'email_password')
WEB_HOST = config.get('webserver', 'web_hostname')
WEB_ROOT = config.get('webserver', 'web_filesystem_root')
ADMIN_EMAIL = config.get('system', 'admin_email')
class Post(object):
title = ''
slug = ''
author = ''
date = ''
url = ''
def __iter__(self):
yield 'title', self.title
yield 'slug', self.slug
yield 'author', self.author
yield 'date', self.date
yield 'url', self.url
def unpack_message(uid, message, blog_dir):
email_body = ''
html_body = ''
text_body = ''
counter = 1
audio_filename = None
audio_length = 0
for part in message.walk():
if part.get_content_maintype() == 'multipart':
continue
# extract message body
if part.get_content_type() == 'text/html':
# TODO: remove any containing head/body tags
html_body = part.get_payload(decode=True)
if part.get_content_type() == 'text/plain':
text_body += part.get_payload(decode=True)
filename = part.get_filename()
if not filename:
ext = mimetypes.guess_extension(part.get_content_type())
if not ext:
# Use a generic bag-of-bits extension
ext = '.bin'
filename = 'part-%03d%s' % (counter, ext)
filename = '%s-%s' % (uid, filename)
# only store files we know what to do with
store_file = False
# caps just makes comparisons harder
filename = filename.lower()
# handle images
if filename.find('.jpg') > 0 or filename.find('.jpeg') > 0 or filename.find('.png') > 0 or filename.find('.gif') > 0 or filename.find('.pdf') > 0:
store_file = True
if part.get('Content-ID'):
cid = 'cid:%s' % part.get('Content-ID')[1:-1]
# if we can find the file embedded, update the link
if html_body.find(cid) > -1:
# re-write CID img tag to use stored filename
html_body = html_body.replace(cid, 'assets/%s' % filename)
else:
# otherwise, just embed the file
email_body = email_body + '<a href=\'assets/%s\'><img src=\'assets/%s\'></a>' % (filename, filename)
# handle video
if filename.find('.mov') > 0 or filename.find('.mp4') > 0 or filename.find('.ogg') > 0 :
store_file = True
email_body = email_body + '<video controls><source src=\'assets/%s\'></video>' % filename
# handle audio
if filename.find('.mp3') > 0 or filename.find('.wav') > 0 or filename.find('.m4a') > 0:
store_file = True
email_body = email_body + '<audio controls><source src=\'assets/%s\'></audio>' % filename
audio_filename = filename
# There might be a better way to get this number...
audio_length = len(part.get_payload(decode=True))
if store_file:
counter += 1
fp = open(os.path.join(blog_dir, 'assets', filename), 'wb')
fp.write(part.get_payload(decode=True))
fp.close()
if html_body:
email_body = html_body + email_body
else:
email_body = text_body + email_body
return {"email_body": email_body, "audio_filename": audio_filename, "audio_length": audio_length}
def send_notification(destination_email, subject, message):
# assemble email
message = MIMEText(message)
message['Subject'] = subject
message['From'] = EMAIL_ADDRESS
message['To'] = destination_email
# send
s = smtplib.SMTP(SMTP_SERVER + ':' + SMTP_PORT)
s.ehlo()
s.starttls()
s.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
s.sendmail(EMAIL_ADDRESS, destination_email, message.as_string())
s.quit()
# get messages
imap_search = 'UNSEEN'
suppress_notification = False
if len(sys.argv) > 1:
if sys.argv[1] == 'rebuild':
shutil.copy('index.html', WEB_ROOT)
shutil.copy('podcast.xml', WEB_ROOT)
shutil.copytree('images', WEB_ROOT + '/images')
shutil.copytree('css', WEB_ROOT + '/css')
imap_search = 'ALL'
suppress_notification = True
mailbox = imaplib.IMAP4_SSL(IMAP_SERVER)
mailbox.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
mailbox.select()
result, data = mailbox.uid('search', None, imap_search)
uid_list = data.pop().split(' ')
# if there's no valid uid in the list, skip it
if uid_list[0] != '':
for uid in uid_list:
# global exception handlers like this are for bad programmers
try:
# fetch message
latest_email_uid = uid
result, data = mailbox.uid('fetch', latest_email_uid, '(RFC822)')
raw_email = data[0][1]
email_message = email.message_from_string(raw_email)
email_from = email.utils.parseaddr(email_message['From'])
email_address = email_from[1]
# assemble post components
post_author = email_address.split('@')[0]
post_date = email_message['Date']
post_title = email_message['Subject']
post_slug = unicodedata.normalize('NFKD', unicode(post_title))
post_slug = post_slug.encode('ascii', 'ignore').lower()
post_slug = re.sub(r'[^a-z0-9]+', '-', post_slug).strip('-')
post_slug = re.sub(r'[-]+', '-', post_slug)
# check for blog subdir
email_hash = hashlib.md5()
email_hash.update(email_address)
blog_directory = email_hash.hexdigest()
blog_physical_path = WEB_ROOT + '/' + blog_directory
humane_blog_name = humanhash.humanize(blog_directory)
if not os.path.exists(WEB_ROOT + '/' + blog_directory):
# create directory for new blog
os.makedirs(blog_physical_path)
os.makedirs(os.path.join(blog_physical_path, 'assets'))
# copy over the default stylsheet
shutil.copytree('css', blog_physical_path + '/css')
# create human-readable link to blog directory
os.symlink(blog_directory, os.path.join(WEB_ROOT, humane_blog_name))
# create html blog post index
template = open('postindextemplate.html', 'r').read()
new_index = template
new_index = new_index.replace('{0}', post_author)
new_index = new_index.replace('{1}', blog_directory)
blog_index = open(blog_physical_path + '/index.html', 'w')
blog_index.write(new_index)
blog_index.close()
# create rss blog post index
template = open('postrssindextemplate.xml', 'r').read()
new_index = template
new_index = new_index.replace('{0}', '%s\'s Preposter.us Blog' % post_author)
new_index = new_index.replace('{1}', 'http://%s/%s' % (WEB_HOST, humane_blog_name))
new_index = new_index.replace('{2}', '%s\'s blog on preposter.us' % post_author)
blog_index = open(blog_physical_path + '/rss.xml', 'w')
blog_index.write(new_index)
blog_index.close()
# podcast support - create individual podcast XML
template = open('podcastrssindextemplate.xml', 'r').read()
new_index = template
new_index = new_index.replace('{0}', '%s\'s Preposter.us Podcast' % post_author)
new_index = new_index.replace('{1}', 'http://%s/%s' % (WEB_HOST, humane_blog_name))
new_index = new_index.replace('{2}', '%s\'s podcast on preposter.us' % post_author)
blog_index = open(blog_physical_path + '/podcast.xml', 'w')
blog_index.write(new_index)
blog_index.close()
# add new blog to site index
blog_index_partial = open(WEB_ROOT + '/blogs.html', 'a')
blog_index_partial.write('<li><a href=\'%s\'>%s</a></li>\n' % (humane_blog_name, post_author))
blog_index_partial.close()
if not suppress_notification:
send_notification(email_address, 'Your new Preposter.us blog is ready!', 'You just created a Preposter.us blog, a list of your posts can be found here: http://%s/%s . Find out more about Preposter.us by visiting the project repository at https://github.com/jjg/preposter.us' % (WEB_HOST, humane_blog_name))
post_physical_path = blog_physical_path + '/' + post_slug + '.html'
# parse the actual message
unpacked_message = unpack_message(uid, email_message, blog_physical_path)
post_body = unpacked_message["email_body"]
# if necessary, update post index
if not os.path.exists(post_physical_path):
# update post index partial
post_index_partial = open(blog_physical_path + '/posts.html', 'a')
post_index_partial.write('<li><a href=\'%s.html\'>%s</a> - %s</li>' % (post_slug, post_title, post_date))
post_index_partial.close()
# update post index json
post = Post()
post.title = post_title
post.slug = post_slug
post.author = post_author
post.date = post_date
post.url = 'http://' + WEB_HOST + '/' + humane_blog_name + '/' + post_slug + '.html'
# create a new index or update an existing one
json_index_physical_path = blog_physical_path + '/posts.json'
post_index_obj = {'posts':[]}
if os.path.exists(json_index_physical_path):
post_index_json = open(json_index_physical_path, 'r')
post_index_obj = json.loads(post_index_json.read())
post_index_json.close()
post_index_obj['posts'].append({'post':dict(post)})
post_index_json = open(json_index_physical_path, 'w')
post_index_json.write(json.dumps(post_index_obj))
post_index_json.close()
# update rss feed
rss_physical_path = blog_physical_path + '/rss.xml'
tree = ET.parse(rss_physical_path)
root = tree.getroot()
# add new post
channel = root.find('channel')
item = ET.SubElement(channel, 'item')
item_title = ET.SubElement(item, 'title')
item_link = ET.SubElement(item, 'link')
item_guid = ET.SubElement(item, 'guid')
item_pub_date = ET.SubElement(item, 'pubDate')
item_description = ET.SubElement(item, 'description')
item_title.text = post.title
item_link.text = post.url
item_guid.text = post.url
item_pub_date.text = post.date
item_description.text = 'a post about %s by %s' % (post.title, post.author)
# save changes
tree.write(rss_physical_path)
# podcast support - add post to podcast XML if media is present
if unpacked_message["audio_filename"]:
# unpack media attributes
audio_filename = unpacked_message["audio_filename"]
audio_length = str(unpacked_message["audio_length"])
audio_type = "audio/%s" % audio_filename.split(".")[-1]
audio_url = "http://%s/%s/assets/%s" % (WEB_HOST, humane_blog_name, audio_filename)
# update user's podcast
podcast_physical_path = blog_physical_path + '/podcast.xml'
tree = ET.parse(podcast_physical_path)
root = tree.getroot()
# add new episode
channel = root.find('channel')
item = ET.SubElement(channel, 'item')
item_title = ET.SubElement(item, 'title')
item_link = ET.SubElement(item, 'link')
item_guid = ET.SubElement(item, 'guid')
item_pub_date = ET.SubElement(item, 'pubDate')
item_description = ET.SubElement(item, 'description')
item_enclosure = ET.SubElement(item, 'enclosure')
item_title.text = post.title
item_link.text = post.url
item_guid.text = post.url
item_pub_date.text = post.date
item_description.text = 'an episode about %s by %s' % (post.title, post.author)
# TODO: add extended podcast attributes
item_enclosure.set("url", audio_url)
item_enclosure.set("type", audio_type)
item_enclosure.set("length", audio_length)
# save changes
tree.write(podcast_physical_path)
# update site-wide podcast
# TODO: this could be DRY'd up
podcast_physical_path = WEB_ROOT + '/podcast.xml'
tree = ET.parse(podcast_physical_path)
root = tree.getroot()
# add new episode
channel = root.find('channel')
item = ET.SubElement(channel, 'item')
item_title = ET.SubElement(item, 'title')
item_link = ET.SubElement(item, 'link')
item_guid = ET.SubElement(item, 'guid')
item_pub_date = ET.SubElement(item, 'pubDate')
item_description = ET.SubElement(item, 'description')
item_enclosure = ET.SubElement(item, 'enclosure')
item_title.text = post.title
item_link.text = post.url
item_guid.text = post.url
item_pub_date.text = post.date
item_description.text = 'an episode about %s by %s' % (post.title, post.author)
# TODO: add extended podcast attributes
item_enclosure.set("url", audio_url)
item_enclosure.set("type", audio_type)
item_enclosure.set("length", audio_length)
# save changes
tree.write(podcast_physical_path)
# write post to disk
post_template = open('posttemplate.html', 'r').read()
new_post = post_template
new_post = new_post.replace('{0}', post_title)
new_post = new_post.replace('{1}', post_author)
new_post = new_post.replace('{2}', post_body)
# TODO: format this date to something prettier
new_post = new_post.replace('{3}', post_date)
post_file = open(post_physical_path, 'w')
post_file.write(new_post)
post_file.close()
if not suppress_notification:
send_notification(email_address, 'Preposter.us Post Posted!', 'Your post \"%s\" has been posted, you can view it here: http://%s/%s/%s.html' % (post_title, WEB_HOST, humane_blog_name, post_slug))
except:
print '****************************************'
print traceback.format_exc()
print raw_email
print '****************************************'