______________________________________________

                         AN IDEA FOR BRINGING PHLOGGING TO THE MASSES

                                                                 lro
                        ______________________________________________


                                                       <2019-01-30 Wed>


Table of Contents
_________________

An idea
The Prototype
. Imports
. Global vars
. Getting email
. Mapping emails to users
. From
. Is that email address in the dictionary?
. Phlog post
. Write phlog post to file
. Update gophermap
. New Phlog entry
. iterating
Conclusion





An idea
=======

 I had a what I think might be a good idea to help people get setup
 phlogging, and thats an email based phlogging service. Where a user
 would email say [email protected] and the subject is their chosen
 title, and the cotents of the emial is the phlog post.

 Each user would be identified by their email address, registrations
 would probably happen on a webpage, with a captcha to stop spammers
 etc.

 I would have each user have a user on the server, and use gophernicus
 to display the user gopher directory to host the phlog. An email bot
 would just watch the inbox of [email protected] and create the phlog
 post, and move it to the user gopher directory, and update the
 gophermap.

 Then we can have a master page at example.com:70 with the most
 recently posted phlogs.


The Prototype
=============

 I figured a good course of action was to setup a prototype using my
 sdf account. I will leave the web based registration for later as that
 seems the most trivial, I am more interested in getting what is
 essentially an email bot setup to watch my email and add phlog posts.

 For ease of writing and ease of development by others, i'm going to
 write it in python. Not a language I've used alot, but this should
 make things easier for others to jump in and modify and run the code
 themselves.

 What my initial implementation does is look for unread emails in a
 mailfile, and create a file named with todays date in that users
 directory as the phlog post. And updates the gophermap with the new
 phlog entry at the top with the subject of the email as a nice little
 description/title.


Imports
~~~~~~~

 I need to import a few libraries for various things, like reading the
 mailbox file, importing json objects from a file, and getting the
 date/time.

 ,----
 | #!/usr/bin/env python3
 |
 | ###################################################################
 | # To the extent possible under law, the person who associated CC0 #
 | # with phlog_bot.py has waived all copyright and related or       #
 | # neighboring rights to phlog_bot.py.                             #
 | #                                                                 #
 | # https://creativecommons.org/publicdomain/zero/1.0/              #
 | ###################################################################
 |
 | # NO WARRANTY!!!!
 | # PLEASE DON'T RUN THIS SCRIPT UNLESS YOU HAVE BACKUPS OF YOUR
 | # MAILFILE OR ANYTHING ELSE THIS SCRIPT TOUCHES PLEASE.
 |
 | import mailbox
 | import json
 | import time
 | import datetime
 | import os
 |
 `----


Global vars
~~~~~~~~~~~

 Global variables.

 ,----
 | gopher_dir = "/sdf/arpa/gm/l/lro/test"
 | userdictionaryfile = "users.json"
 | mailboxf = "/mail/lro"
 | today = datetime.date.today()
 | today_fmt = today.strftime("%Y")+"-"+today.strftime("%m")+"-"+today.strftime("%d")
 |
 `----


Getting email
~~~~~~~~~~~~~

 The first step is to open the email and get the contents, then collect
 the unread emails. For this prototype i'm just going to read my sdf
 mailspool file at `/mail/lro'.

 To get the unread emails I need to get all the mail from the file and
 return all the mail that doesn't have the read (R) flag set and mark
 all the new mail as read. Which as it turns out is a bit weird to set
 the read flag in mail, which is why I have to keep track of the index
 into /lromailbox/.

 See if you just use `add_flag' on /message/, it doesn't write that
 back to /lromailbox/, so to get around that I set the index into
 /lromailbox/ to the altered /message/, then when I write /lromailbox/
 back to disk the read flag is now set.

 And we return a list of all the unread mail.

 ,----
 | def get_unread_emails(mailboxfile):
 |     unread = []
 |     if not os.path.isfile(mailboxfile):
 |         print("Mailboxfile doesn't exist: %s" % (mailboxfile))
 |         return unread
 |
 |     mailboxfp = mailbox.mbox(mailboxfile)
 |     ind = 0
 |     for message in mailboxfp:
 |         flags = message.get_flags()
 |         if flags.find('R') == -1:
 |             unread.append(message)
 |             message.add_flag('R')
 |             mailboxfp[ind] = message
 |         ind = ind + 1
 |
 |     mailboxfp.lock()
 |     mailboxfp.flush()
 |     mailboxfp.unlock()
 |     mailboxfp.close()
 |     return unread
 |
 `----


Mapping emails to users
~~~~~~~~~~~~~~~~~~~~~~~

 I need to store a dictionary of mappings of email addresses to
 usernames. At this point i've chosen to go with JSON so I can read it
 in easily from a file on disk.

 For the time being this is file is handled manually, but in the
 production version will be handled by the registration program.

 ,----
 | {"[email protected]" : "lrojr", "[email protected]" : "lrosr"}
 `----

 ,----
 | def load_dictionary(dictfile):
 |     if not os.path.isfile(dictfile):
 |         return []
 |
 |     usersfile = open(dictfile)
 |     userdictionary = json.load(usersfile)
 |     usersfile.close()
 |     return userdictionary
 |
 `----


From
~~~~

 What we need next is a function that gets the from address from each
 unread message and transforms it to lower case so that the output can
 be used to search through /userdictionary/.

 ,----
 | def get_from_lower(message):
 |     From = message.get_from()
 |     return From.split(" ")[0].lower()
 |
 `----


Is that email address in the dictionary?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 Checks if an email is one of our registered users by trying to get
 their username from the dictionary, returning the username if they
 are, otherwise returns False.

 ,----
 | def user_in_dict(email, dictionary):
 |     try:
 |         return dictionary[email]
 |     except KeyError:
 |         return False
 |
 `----


Phlog post
~~~~~~~~~~

 Gets new phlog post text from email.

 ,----
 | def phlog_post_contents(email):
 |     return email.get_payload()
 |
 `----


Write phlog post to file
~~~~~~~~~~~~~~~~~~~~~~~~

 Writes the phlog post file to disk. /filename/ includes full path to
 file.

 If file already exists then add a "-x" to the end.

 ,----
 | def write_phlog_post(filename, contents):
 |     ind = 0
 |     justfilenamenotpath = filename.split("/")[-1]
 |     while os.path.isfile(filename):
 |         if ind == 99:
 |             break
 |         filename = filename.split(".")[0].split("_")[0]+"_"+str(ind)+".txt"
 |         justfilenamenotpath = filename.split("/")[-1]
 |
 |     with open(filename, "w") as phlogfile:
 |         phlogfile.write(contents)
 |
 |     return justfilenamenotpath
 |
 `----


Update gophermap
~~~~~~~~~~~~~~~~

 Updates the phlog gophermap with the new phlog post.

 /gophermap/ is the full path and filename for the gophermap.

 ,----
 | def update_gophermap(gophermap, filename, description):
 |     entry = "0%s    %s" % (description, filename)
 |
 |     with open(gophermap, "r") as gfp:
 |         buf = gfp.readlines()
 |
 |     with open(gophermap, "w") as gfp:
 |         for line in buf:
 |             if "==================================================" in line:
 |                 line = line+"\n"+entry+"\n"
 |             gfp.write(line)
 |
 `----


New Phlog entry
~~~~~~~~~~~~~~~

 Creates a new phlog entry, and updates the gophermap.

 ,----
 | def new_phlog_entry(message):
 |     email = get_from_lower(message)
 |     username = user_in_dict(email, userdictionary)
 |     if not username:
 |         print("User: %s not in user dictionary" % (email))
 |         return
 |     phlog_title = message['subject']
 |     phlog_filename = today_fmt + ".txt"
 |     phlog_contents = phlog_post_contents(message)
 |     phlog_dir = gopher_dir+"/"+username
 |     phlog_gophermap = phlog_dir+"/gophermap"
 |     phlog_filename = write_phlog_post(phlog_dir+"/"+phlog_filename, phlog_contents)
 |     update_gophermap(phlog_gophermap, phlog_filename, phlog_title+" <"+today_fmt+">")
 |     update_gophermap(gopher_dir+"/gophermap", username+"/"+phlog_filename, username+" - "+phlog_title+" <"+today_fmt+">")
 |     print("Added phlog post for %s" % (username))
 |
 `----


iterating
~~~~~~~~~

 The final step is to iterate over all the unread emails and make them
 phlog posts.

 ,----
 | userdictionary = load_dictionary(userdictionaryfile)
 |
 | for message in get_unread_emails(mailboxf):
 |     new_phlog_entry(message)
 |
 `----


Conclusion
==========

 So thats the initial prototype done, I don't know what I am going to
 do with it though, whether I should host a small gopher server and set
 up a new email or not. If someone else wants too, bloody go for it!

 This could also be used personally by anyone as well, just add a
 snippet to the /get_unread_emails/ that checks the subject for ones
 that start with [PHLOG_POST] or something and have it watch your mail
 account.

 If your worried about security you could always GPG sign the emails
 and have the dictionary verify each email, but that adds alot of
 complexity.

 Feature requests welcome!