aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMatěj Cepl <mcepl@cepl.eu>2023-05-25 10:15:47 +0200
committerMatěj Cepl <mcepl@cepl.eu>2023-05-25 11:01:18 +0200
commitec4b49d843e67b31b33ac81bef55346353f1d04c (patch)
tree5f60ffae4d6ebe180c5ee4d51d468bf154535251 /src
parent8006d981ce26fe8c1140e33b9476c08470d59f30 (diff)
downloadpygn-ec4b49d843e67b31b33ac81bef55346353f1d04c.tar.gz
refactor: rearrange the project to the src/ layout.0.10.3
Fix also pyproject.toml to generate what seems right. Add the explicit dependency on nntplib for Python >= 3.12 (gh#python/cpython!104894). Fixes: https://todo.sr.ht/~mcepl/pygn/7
Diffstat (limited to 'src')
-rw-r--r--src/mail2news.py195
-rw-r--r--src/news2mail.py174
-rwxr-xr-xsrc/pygm2n99
-rwxr-xr-xsrc/pygn2m126
-rw-r--r--src/whitelist.py99
-rw-r--r--src/wlp.py22
-rw-r--r--src/wlp_parser.py69
7 files changed, 784 insertions, 0 deletions
diff --git a/src/mail2news.py b/src/mail2news.py
new file mode 100644
index 0000000..6e8fe53
--- /dev/null
+++ b/src/mail2news.py
@@ -0,0 +1,195 @@
+"""Mail to news gateway script. Copyright 2000 Cosimo Alfarano
+
+Author: Cosimo Alfarano
+Date: September 16 2000
+
+mail2news.py - (C) 2000 by Cosimo Alfarano <Alfarano@Students.CS.UniBo.It>
+You can use this software under the terms of the GPL. If we meet some day,
+and you think this stuff is worth it, you can buy me a beer in return.
+
+Thanks to md for this useful formula. Beer is beer.
+
+Gets news email and sends it via SMTP.
+
+class mail2news is hopefully conform to rfc850.
+
+"""
+import io
+from collections import OrderedDict
+import email
+import email.policy
+import logging
+import nntplib
+import os
+from re import findall
+from socket import gethostbyaddr, gethostname
+import sys
+import tempfile
+
+
+#logging.basicConfig(level=logging.DEBUG)
+# This is the single source of Truth
+# Yes, it is awkward to have it assymetrically here
+# and not in news2mail as well.
+__version__ = '0.10.3'
+__description__ = 'The Python Gateway Script: news2mail mail2news gateway'
+
+
+class mail2news(object):
+ """news to mail gateway class"""
+
+ def __init__(self, options):
+ # newsgroups = None # Newsgroups: local.test,local.moderated...
+ # approved = None # Approved: kame@aragorn.lorien.org
+ if 'NNTPHOST' in os.environ:
+ self.newsserver = os.environ['NNTPHOST']
+ else:
+ self.newsserver = 'localhost'
+
+ self.port = 119
+ self.user = None
+ self.password = None
+ self.verbose = options.verbose
+ logging.debug('self.verbose = %s', self.verbose)
+
+ self.hostname = gethostbyaddr(gethostname())[0]
+
+ self.heads_dict, self.smtpheads, self.nntpheads = {}, {}, {}
+ self.message = self.__readfile(options)
+
+ self.message['X-Gateway'] = 'pyg {0} {1}'.format(VERSION, DESC)
+
+ def __add_header(self, header, value, msg=None):
+ if msg is None:
+ msg = self.message
+ if value:
+ msg[header] = value.strip()
+
+ def __readfile(self, opt):
+ message = email.message_from_file(sys.stdin, policy=email.policy.SMTP)
+
+ if (len(message) == 0) \
+ and message.get_payload().startswith('/'):
+ msg_file_name = message.get_payload().strip()
+ del message
+ with open(msg_file_name, 'r') as msg_file:
+ message = email.message_from_file(msg_file, policy=email.policy.SMTP)
+
+ # introduce nntpheads
+ self.__add_header('Newsgroups', opt.newsgroup, message)
+ self.__add_header('Approved', opt.approver, message)
+
+ return message
+
+ def renameheads(self):
+ """rename headers such as Resent-*: to X-Resent-*:
+
+ headers renamed are useless or not rfc 977/850 copliant
+ handles References/In-Reply-To headers
+ """
+ try:
+
+ for key in list(self.message.keys()):
+ if key.startswith('Resent-'):
+ if ('X-' + key) in self.message:
+ self.message['X-Original-' + key] = \
+ self.message['X-' + key]
+ self.message['X-' + key] = self.message[key]
+ del self.message[key]
+
+ # In rfc822 References: is considered, but many MUA doen't put it.
+ if ('References' not in self.message) and \
+ ('In-Reply-To' in self.message):
+ print(self.message['In-Reply-To'])
+
+ # some MUA uses msgid without '<' '>'
+# ref = findall('([^\s<>\']+@[^\s<>;:\']+)', \
+ # but I prefer use RFC standards
+ ref = findall('(<[^<>]+@[^<>]+>)',
+ self.message['In-Reply-To'])
+
+ # if found, keep first element that seems a Msg-ID.
+ if (ref and len(ref)):
+ self.message['References'] = '%s\n' % ref[0]
+
+ except KeyError as message:
+ print(message)
+
+ def removeheads(self, heads=None):
+ """remove headers like Xref: Path: Lines:
+ """
+
+ try:
+ # removing some others useless headers .... (From is not From:)
+
+ rmheads = ['Received', 'From ', 'NNTP-Posting-Host',
+ 'X-Trace', 'X-Compliants-To', 'NNTP-Posting-Date']
+ if heads:
+ rmheads.append(heads)
+
+ for head in rmheads:
+ if head in self.message:
+ del self.message[head]
+
+ if 'Message-Id' in self.message:
+ msgid = self.message['Message-Id']
+ del self.message['Message-Id']
+ self.message['Message-Id'] = msgid
+ else:
+ msgid = '<pyg.%d@tuchailepuppapera.org>\n' % (os.getpid())
+ self.message['Message-Id'] = msgid
+
+ except KeyError as message:
+ print(message)
+
+ def sortheads(self):
+ """make list sorted by heads: From: To: Subject: first,
+ others, X-*, X-Resent-* last"""
+
+ heads_dict = OrderedDict(self.message)
+ for hdr in list(self.message.keys()):
+ del self.message[hdr]
+
+ # put at top
+ head_set = ('Newsgroups', 'From', 'To', 'X-To', 'Cc', 'Subject',
+ 'Date', 'Approved', 'References', 'Message-Id')
+
+ logging.debug('heads_dict = %s', heads_dict)
+ for k in head_set:
+ if k in heads_dict:
+ self.__add_header(k, heads_dict[k])
+
+ for k in heads_dict:
+ if not k.startswith('X-') and not k.startswith('X-Resent-') \
+ and k not in head_set:
+ self.__add_header(k, heads_dict[k])
+
+ for k in heads_dict:
+ if k.startswith('X-'):
+ self.__add_header(k, heads_dict[k])
+
+ for k in heads_dict:
+ if k.startswith('X-Resent-'):
+ self.__add_header(k, heads_dict[k])
+
+ def sendemail(self):
+ "Talk to NNTP server and try to send email."
+ # readermode must be True, otherwise we don't have POST command.
+ server = nntplib.NNTP(self.newsserver, self.port, self.user,
+ self.password, readermode=True)
+
+ logging.debug('self.verbose = %s', self.verbose)
+ if self.verbose:
+ server.set_debuglevel(2)
+
+ msg_bytes = self.message.as_bytes()
+ try:
+ server.post(io.BytesIO(msg_bytes))
+ except UnicodeEncodeError:
+ with tempfile.NamedTemporaryFile(suffix="eml", prefix="failed_msg",
+ delete=False) as tmpf:
+ tmpf.write(msg_bytes)
+ logging.info(f"failed file name = {tmpf.name}")
+ logging.exception("Failed to convert message!")
+
+ server.quit()
diff --git a/src/news2mail.py b/src/news2mail.py
new file mode 100644
index 0000000..33d2590
--- /dev/null
+++ b/src/news2mail.py
@@ -0,0 +1,174 @@
+"""News to mail gateway script. Copyright 2000 Cosimo Alfarano
+
+Author: Cosimo Alfarano
+Date: June 11 2000
+
+news2mail.py - (C) 2000 by Cosimo Alfarano <Alfarano@Students.CS.UniBo.It>
+You can use this software under the terms of the GPL. If we meet some day,
+and you think this stuff is worth it, you can buy me a beer in return.
+
+Thanks to md for this useful formula. Beer is beer.
+
+Gets news article and sends it via SMTP.
+
+class news2mail is hopefully conform to rfc822.
+
+normal (what pygs does) operations flow is:
+1) reads from stdin NNTP article (readfile)
+2) divide headers and body (parsearticle)
+3) merges NNTP and SMTP heads into a unique heads
+4) adds, renames and removes some heads
+5) sorts remaining headers starting at top with Received: From: To: Subject:
+ Date:, normal headers ending with X-* and Resent-* headers.
+
+"""
+from __future__ import absolute_import
+from __future__ import print_function
+from collections import OrderedDict
+import email
+import email.policy
+import smtplib
+from socket import gethostbyaddr, gethostname
+import sys
+import time
+from mail2news import VERSION, DESC
+
+
+# logging.basicConfig(level=logging.DEBUG)
+class news2mail(object):
+ """news to mail gateway class"""
+
+ def __init__(self, verbose=False):
+ self.wlfile = None
+ self.logfile = None
+ self.verbose = verbose
+
+ self.sender = ''
+ self.rcpt = ''
+ self.envelope = ''
+
+ self.smtpserver = 'localhost'
+
+ self.hostname = gethostbyaddr(gethostname())[0]
+
+ self.heads_dict = {}
+ self.article, self.headers, self.body = [], [], []
+ self.message = self.__addheads(
+ email.message_from_file(sys.stdin, policy=email.policy.SMTP))
+
+ def __addheads(self, msg):
+ """add new header like X-Gateway: Received:
+ """
+
+ msg['X-Gateway'] = 'pyg {0} {1}'.format(VERSION, DESC)
+
+ # to make Received: header
+ t = time.ctime(time.time())
+
+ if time.daylight:
+ tzone = time.tzname[1]
+ else:
+ tzone = time.tzname[0]
+
+ # An example from debian-italian:
+ # Received: from murphy.debian.org (murphy.debian.org [216.234.231.6])
+ # by smv04.iname.net (8.9.3/8.9.1SMV2) with SMTP id JAA26407
+ # for <kame.primo@innocent.com> sent by
+ # <debian-italian-request@lists.debian.org
+
+ tmp = 'from GATEWAY by ' + self.hostname + \
+ ' with pyg' + \
+ '\n\tfor <' + self.rcpt + '> ; ' + \
+ t + ' (' + tzone + ')\n'
+
+ msg['Received'] = tmp
+
+ return msg
+
+ def __renameheads(self):
+ """remove headers like Xref: Path: Lines:
+ rename headers such as Newsgroups: to X-Newsgroups:
+
+ headers renamed are useless or not rfc 822 copliant
+ """
+ try:
+ if 'Newsgroups' in self.message:
+ self.message['X-Newsgroups'] = \
+ self.message['Newsgroups']
+ del self.message['Newsgroups']
+
+ if 'NNTP-Posting-Host' in self.message:
+ self.message['X-NNTP-Posting-Host'] = \
+ self.message['NNTP-Posting-Host']
+ del self.message['NNTP-Posting-Host']
+ except KeyError as ex:
+ print(ex)
+
+ try:
+ # removing some others useless headers ....
+ # that includes BOTH 'From ' and 'From'
+ # 'Sender is usually set by INN, if ng is moderated...
+ for key in ('Approved', 'From', 'Xref', 'Path', 'Lines', 'Sender'):
+ if key in self.message:
+ del self.message[key]
+
+ if 'Message-id' in self.message:
+ msgid = self.message['Message-id']
+ del self.message['Message-id']
+ self.message['Message-Id'] = msgid
+ else:
+ # It should put a real user@domain
+ self.heads_dict['Message-Id'] = 'pyg@puppapera.org'
+
+ if 'References' in self.message and \
+ 'In-Reply-To' not in self.message:
+ refs = self.message['References'].split()
+ self.message['In-Reply-To'] = refs[-1]
+
+ except KeyError as message:
+ print(message)
+
+ def __sortheads(self):
+ """make list sorting heads, Received: From: To: Subject: first,
+ others, X-*, Resent-* last"""
+
+ # put at top
+ header_set = ('Received', 'From', 'To', 'Subject', 'Date')
+
+ heads_dict = OrderedDict(self.message)
+ for hdr in list(self.message.keys()):
+ del self.message[hdr]
+
+ for k in header_set:
+ if k in heads_dict:
+ self.message[k] = heads_dict[k]
+
+ for k in heads_dict:
+ if not k.startswith('X-') and not k.startswith('Resent-') \
+ and k not in header_set:
+ self.message[k] = heads_dict[k]
+
+ for k in heads_dict:
+ if k.startswith('X-'):
+ self.message[k] = heads_dict[k]
+
+ for k in heads_dict:
+ if k.startswith('Resent-'):
+ self.message[k] = heads_dict[k]
+
+ def process_message(self):
+ """phase 3:
+ format rfc 822 headers from input article
+ """
+ self.__renameheads() # remove other heads
+ self.__sortheads()
+
+ def sendarticle(self):
+ """Talk to SMTP server and try to send email."""
+ s = smtplib.SMTP(self.smtpserver)
+ s.set_debuglevel(self.verbose)
+
+ s.sendmail(self.envelope, self.rcpt, self.message.as_bytes())
+
+ s.quit()
+
diff --git a/src/pygm2n b/src/pygm2n
new file mode 100755
index 0000000..3629857
--- /dev/null
+++ b/src/pygm2n
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""News to mail gateway script. Copyright 2000 Cosimo Alfarano
+
+Author: Cosimo Alfarano
+Date: June 11 2000
+
+pygs - Copyright 2000 by Cosimo Alfarano <Alfarano@Students.CS.UniBo.It>
+You can use this software under the terms of the GPL. If we meet some day,
+and you think this stuff is worth it, you can buy me a beer in return.
+
+Thanks to md for this useful formula. Beer is beer.
+
+Gets news article and sends it via SMTP.
+"""
+from __future__ import print_function
+
+import argparse
+import logging
+import mail2news
+import nntplib
+import sys
+
+# logging.basicConfig(level=logging.DEBUG)
+
+def parse_cmdline():
+ parser = argparse.ArgumentParser(
+ description='%s version %s - Copyright 2000 Cosimo Alfarano\n%s' %
+ ('pyg', mail2news.VERSION, mail2news.DESC))
+
+ parser.add_argument('-s', '--newsserver', default='')
+ parser.add_argument('-a', '--approver', default='',
+ help="address of moderator/approver")
+ parser.add_argument('-n', '--newsgroup', default='',
+ help='newsgroup[s] (specified as comma separated ' +
+ 'without spaces list)', required=True)
+ parser.add_argument('-u', '--user', default='',
+ help='NNTP server user (for authentication)')
+ parser.add_argument('-p', '--password', default='',
+ help='NNTP server password (for authentication)')
+ parser.add_argument('-P', '--port', default='')
+ parser.add_argument('-e', '--envellope', default='')
+ parser.add_argument('-l', '--logfile')
+
+ parser.add_argument('-T', '--test', action='store_true',
+ help='test mode (not send article via NNTP)')
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='verbose output ' +
+ '(usefull with -T option for debugging)')
+
+ args = parser.parse_args()
+
+ if not args.newsgroup:
+ raise argparse.ArgumentError('Error: Missing Newsgroups\n')
+
+ return args
+
+
+"""main is structured in 4 phases:
+ 1) check and set pyg's internal variables
+ 2) check whitelist for users' permission
+ 3) format rfc 822 headers from input article
+ 4) open smtp connection and send e-mail
+"""
+
+
+try:
+
+ """phase 1:
+ check and set pyg's internal variables
+ """
+ opt = parse_cmdline()
+
+ m2n = mail2news.mail2news(opt)
+ owner = None
+
+ """phase 3:
+ format rfc 822 headers from input article
+ """
+ m2n.renameheads() # rename useless heads
+ m2n.removeheads() # remove other heads
+
+ m2n.sortheads() # sort remaining heads :)
+
+ if opt.verbose:
+ print(m2n.message.as_string())
+
+ logging.debug('m2n.payload = len %d', len(m2n.message.get_payload()))
+ if len(m2n.message.get_payload()) > 0:
+# wl.logmsg(m2n.heads_dict,wl.ACCEPT,owner)
+ if not opt.test:
+ try:
+ resp = m2n.sendemail()
+ except nntplib.NNTPError as ex:
+ print(ex)
+
+except KeyboardInterrupt:
+ print('Keyboard Interrupt')
+ sys.exit(0)
diff --git a/src/pygn2m b/src/pygn2m
new file mode 100755
index 0000000..a2c07cb
--- /dev/null
+++ b/src/pygn2m
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""News to mail gateway script. Copyright 2000 Cosimo Alfarano
+
+Author: Cosimo Alfarano
+Date: June 11 2000
+
+pygs - Copyright 2000 by Cosimo Alfarano <Alfarano@Students.CS.UniBo.It>
+You can use this software under the terms of the GPL. If we meet some day,
+and you think this stuff is worth it, you can buy me a beer in return.
+
+Thanks to md for this useful formula. Beer is beer.
+
+Gets news article from stdin and sends it via SMTP.
+"""
+from __future__ import print_function
+
+import argparse
+from mail2news import VERSION, DESC
+import news2mail
+import os
+import sys
+import whitelist
+
+
+def parse_cmdline():
+ """
+ set a dictionary with smtp new header in gw parameter (gw.smtpheads)
+ return (test,verbose) boolean tuple
+ """
+ parser = argparse.ArgumentParser(
+ description='pyg version %s - Copyright 2000 Cosimo Alfarano\n%s' %
+ (VERSION, DESC))
+
+ parser.add_argument('-H', '--smtpserver', default='')
+ parser.add_argument('-s', '--sender', required=True, default='')
+ parser.add_argument('-e', '--envelope', default='')
+ parser.add_argument('-t', '--to', dest='rcpt', required=True)
+ parser.add_argument('-w', '--wlfile')
+ parser.add_argument('-l', '--logfile')
+
+ parser.add_argument('-T', '--test',
+ help='test mode (not send article via SMTP)',
+ action='store_true')
+ parser.add_argument('-v', '--verbose', help='verbose output',
+ action='store_true')
+
+ opts = parser.parse_args()
+
+# By rfc822 [Resent-]Sender: should be ever set, unless == From:
+# (not this case). Should be a human, while [Resent-]From: may be a program.
+
+ if opts.rcpt == '' or opts.sender == '':
+ raise argparse.ArgumentError('missing command line option')
+
+ if opts.envelope == '' and opts.sender != '':
+ opts.envelope = opts.sender
+
+ return opts
+
+
+"""main is structured in 4 phases:
+ 1) check and set pyg's internal variables
+ 2) check whitelist for users' permission
+ 3) format rfc 822 headers from input article
+ 4) open smtp connection and send e-mail
+"""
+
+"""phase 1:
+check and set pyg's internal variables
+"""
+
+# it returns only test, other parms are set directly in the actual
+# parameter
+args = parse_cmdline()
+
+n2m = news2mail.news2mail(verbose=args.verbose)
+owner = None
+
+# check if n2m has some file prefercences set on commandline
+if args.wlfile is None:
+ wl = os.path.expanduser(os.path.join(os.path.dirname(__file__), 'pyg.whitelist'))
+else:
+ wl = args.wlfile
+
+if args.logfile is None:
+ log = os.path.expanduser(os.path.join(os.path.dirname(__file__), 'pyg.log'))
+else:
+ log = args.logfile
+
+wl = whitelist.whitelist(wl, log)
+
+"""phase 2:
+check whitelist for user's permission
+"""
+
+# make a first check of From: address
+owner = wl.checkfrom(n2m.message['From'])
+if owner is None:
+ if sys.stdin.isatty() == 1 or args.test:
+ print ('"%s" is not in whitelist!' % (n2m.message['From'][:-1]))
+ else:
+ wl.logmsg(n2m.nntpheads, wl.DENY)
+
+ # if verbose, I want to print out headers, so I can't
+ # exit now.
+ if not args.verbose:
+ sys.exit(1)
+
+# Reformat the message
+n2m.process_message()
+
+# prints formatted email message only (without send) if user wants
+if args.verbose:
+ print(n2m.message.as_string())
+
+if owner is None:
+ sys.exit(1)
+
+"""phase 4:
+open smtp connection and send e-mail
+"""
+
+wl.logmsg(n2m.heads_dict, wl.ACCEPT, owner)
+if not args.test:
+ n2m.sendarticle()
diff --git a/src/whitelist.py b/src/whitelist.py
new file mode 100644
index 0000000..4e02f0e
--- /dev/null
+++ b/src/whitelist.py
@@ -0,0 +1,99 @@
+"""News to mail gateway script. Copyright 2000 Cosimo Alfarano
+
+Author: Cosimo Alfarano
+Date: June 11 2000
+
+whitelist.py - (C) 2000 by Cosimo Alfarano <Alfarano@Students.CS.UniBo.It>
+You can use this software under the terms of the GPL. If we meet some day,
+and you think this stuff is worth it, you can buy me a beer in return.
+
+Thanks to md for this useful formula. Beer is beer.
+
+whitelist manage a list of trusted user.
+"""
+
+from __future__ import absolute_import
+import logging
+# logging.basicConfig(level=logging.DEBUG)
+import sys
+import time
+
+import wlp
+
+
+class whitelist(object):
+ """whitelist handling class
+
+ Do you really want anyone can post? Ah ah ah.
+ """
+ wl = {}
+
+ # constants
+ DENY = 0
+ ACCEPT = 1
+
+ def __init__(self, wlfile, logfile='pyg.log'):
+ self.logger = logging.getLogger(__name__)
+ self.logger.setLevel(logging.INFO)
+ log_fh = logging.FileHandler(logfile)
+ log_fmt = logging.Formatter(
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+ log_fh.setFormatter(log_fmt)
+ self.logger.addHandler(log_fh)
+
+ # dict is a { ownername : {variable: value}} dictionary of dictionaries
+ with open(wlfile) as inf:
+ self.wl = wlp.mkdict(inf)
+
+ def checkfrom(self, fromhead):
+ """have you permission to be here, sir?"""
+ for owner in self.wl:
+ # here colon after 'From' IS required, because binary module wl
+ # expects it.
+ # TODO: when switching to the python lexxing, remove this
+ # limitation.
+ if fromhead.find(self.wl[owner]['From:']) >= 0:
+ return owner
+ else:
+ return None
+
+ def logmsg(self, heads, ok=DENY, owner=None):
+ """who are walking through my gate?
+ """
+
+ ltime = time.ctime(time.time())
+
+ if time.daylight:
+ tzone = time.tzname[1]
+ else:
+ tzone = time.tzname[0]
+
+ if ok == self.ACCEPT:
+ self.logger.info('Permission Accorded ')
+ else:
+ self.logger.info('Permission Denied ')
+
+ self.logger.info('at %s (%s)', ltime, tzone)
+ if owner is not None:
+ self.logger.debug('\tWLOwner: ' + owner + '')
+ self.logger.debug('\tFrom: ' + heads.get('From', 'NOT PRESENT'))
+ self.logger.debug('\tSubject: ' + heads.get('Subject', 'NOT PRESENT'))
+ self.logger.debug('\tSender: ' + heads.get('Sender', 'NOT PRESENT'))
+ self.logger.debug('\tDate: ' + heads.get('Date', 'NOT PRESENT'))
+
+ # some client create Message-Id other Message-ID.
+ if 'Message-ID' in heads:
+ self.logger.debug('\tMessage-ID: ' + heads.get('Message-ID'))
+ else:
+ self.logger.debug('\tMessage-Id: ' + heads.get('Message-Id',
+ 'NOT PRESENT'))
+
+ # X-Newsgroups: and To: are present if user is trusted, else
+ # Newsgroup: exists since no changes on nntp headers are done.
+ if 'X-Newsgroups' in heads:
+ self.logger.debug('\tTo: ' + heads.get('To', 'NOT PRESENT'))
+ self.logger.debug('\tX-Newsgroups: ' + heads.get('X-Newsgroups',
+ 'NOT PRESENT'))
+ else:
+ self.logger.debug('\tNewsgroups: ' +
+ heads.get('Newsgroups', 'NOT PRESENT'))
diff --git a/src/wlp.py b/src/wlp.py
new file mode 100644
index 0000000..e3bcddd
--- /dev/null
+++ b/src/wlp.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+import wlp_parser
+
+
+def mkdict(infile):
+ dict = {}
+
+ tree = wlp_parser.parser.parse(
+ wlp_parser.lexer.lex(infile.read()))
+
+ for subtree in tree:
+ current_key = subtree[0].getstr().strip('<>')
+ if current_key not in dict:
+ dict[current_key] = {}
+
+ for key, value in subtree[1]:
+ key = key.getstr()
+ value = value.getstr().strip("'\"")
+ dict[current_key][key] = value
+
+ return dict
diff --git a/src/wlp_parser.py b/src/wlp_parser.py
new file mode 100644
index 0000000..e2e0d19
--- /dev/null
+++ b/src/wlp_parser.py
@@ -0,0 +1,69 @@
+from __future__ import absolute_import
+import rply
+
+__lg = rply.LexerGenerator()
+# Add takes a rule name, and a regular expression that defines the rule.
+__lg.add("OWNER", r'<[a-zA-Z0-9_.+-]+@[a-zA-Z0-9._-]+>')
+__lg.add("VAL", r'[\'`"][a-zA-Z0-9@_+.<>() -]+[\'`"]')
+__lg.add("VAR", r'[a-zA-Z0-9_<>-]+[:]?')
+
+__lg.ignore(r"\s+")
+__lg.ignore(r'[{}=]+')
+
+lexer = __lg.build()
+
+__pg = rply.ParserGenerator(['OWNER', 'VAL', 'VAR'],
+ cache_id='wlp_parser')
+
+"""
+ $accept ::= block $end
+ block ::= blockstatement
+ | block blockstatement
+ blockstatement ::= owner '{' commandline '}'
+ commandline ::= command
+ | commandline command
+ command ::= varpart '=' valpart
+ owner ::= OWNERID
+ varpart ::= VARID
+ valpart ::= VALID
+"""
+
+
+@__pg.production('main : block')
+def main(p):
+ return p[0]
+
+
+@__pg.production('block : blockstatement')
+def block(p):
+ return p
+
+
+@__pg.production('block : block blockstatement')
+def block_blockstatement(p):
+ p[0].append(p[1])
+ return p[0]
+
+
+@__pg.production('blockstatement : OWNER commandline')
+def blockstatement(p):
+ return [p[0], p[1]]
+
+
+@__pg.production('commandline : command')
+def commandline(p):
+ return p
+
+
+@__pg.production('commandline : commandline command')
+def commandline_command(p):
+ p[0].append(p[1])
+ return p[0]
+
+
+@__pg.production('command : VAR VAL')
+def command(p):
+ return p
+
+
+parser = __pg.build()