Index: bin/run_tests
===================================================================
--- bin/run_tests	(revision 1)
+++ bin/run_tests	(revision 1)
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+#
+# Run the tests for the postman package
+
+import os, sys, unittest
+
+try:
+    from postman.tests import *
+except ImportError:
+    sys.path.append(os.path.join(os.path.dirname(__file__), '../'))
+    try:
+        from postman.tests import *
+    except ImportError:
+        raise SystemExit('Could not find postman on your filesystem')
+    
+unittest.main()
Index: postman/config.py
===================================================================
--- postman/config.py	(revision 1)
+++ postman/config.py	(revision 1)
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+import os
+
+# The default path for storage files
+storage_path = os.path.join(os.path.dirname(__file__), 'db')
+
+# Set to True to set that, by default, only emails from members
+# will go into the list
+private_mailing = True
Index: postman/models.py
===================================================================
--- postman/models.py	(revision 1)
+++ postman/models.py	(revision 1)
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+
+import os
+from tools import validate_email_address
+from storage import JsonStorage
+import config
+
+class Member():
+
+    """
+    Class that defines a mailing list member
+    """
+
+    def __init__(self, address):
+        self.address = self._validate_address(address)
+        
+    def __repr__(self):
+        return "<Member '%s'>" % self.address
+
+    def __str__(self):
+        return self.address
+
+    def _validate_address(self, address):
+        if not validate_email_address(address):
+            raise ValueError(address, ' is not a valid email address')
+        return address
+
+
+class MailingList():
+
+    """
+    Class that defines a mailing list
+    """
+
+    def __init__(self, name, address, members={}, config={}, storage=None):
+        self.name = name
+        self.address = address
+        self.members = members
+        self.config = config
+        self.storage = JsonStorage(os.path.join(config['storage'],
+                                                self.address))
+
+    def __repr__(self):
+        return "<MailingList '%s'>" % self.address
+
+    def __str__(self):
+        return self.address
+
+    def _validate_config(self):
+        if not 'storage' in self.config.keys():
+            self.config['storage'] = os.path.join(config.storage_path,
+                                                  self.address)
+        if not 'archive' in self.config.keys():
+            self.config['archive'] = os.path.join(self.config['storage'],
+                                                  'archive')
+        if not 'private' in self.config.keys():
+            self.config['private'] = config.private
+        
+    def _validate_member_object(self, member):
+        if not isinstance(member, Member):
+            raise TypeError(member, ' is not a valid Member instance')
+        return member
+
+    def _validate_member(self, member):
+        member = self._validate_member_object(member)
+        return member.address in self.members_addresses()
+
+    def _validate_member_by_address(self, address):
+        if not validate_email_address(address):
+            raise ValueError(address, ' is not a valid email address')
+        return address in self.members_addresses()
+
+    def members_addresses(self):
+        return self.members.keys()
+
+    def add_member(self, member):
+        member = self._validate_member_object(member)
+        if self._validate_member(member):
+            return False
+        self.members[member.address] = member
+        return True
+
+    def add_member_by_address(self, address):
+        if self._validate_member_by_address(address):
+            return False
+        member = Member(address)
+        self.members[address] = member
+        return True
+                
+    def delete_member(self, member):
+        member = self._validate_member_object(member)
+        if not self._validate_member(member):
+            return False
+        del self.members[member.address]
+        return True
+
+    def load(self):
+        if self.storage.exists():
+            data = self.storage.read()
+            self.name = data.name
+            self.address = data.address
+            self.members = data.members
+    
+    def save(self):
+        self.storage.write(self)
Index: postman/mta.py
===================================================================
--- postman/mta.py	(revision 1)
+++ postman/mta.py	(revision 1)
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+import sys, email, smtplib
+from datetime import datetime
+from models import MailingList
+import config
+
+class Sendmail():
+    
+    def __init__(self, mailing_list):
+        if not isinstance(mailing_list, MailingList):
+            raise ValueError(mailing_list, ' is not a valid mailing list')
+        self.mailing_list = mailing_list
+        self.suscriptors = self.mailing_list.members_addresses
+        self.reply_address = self.mailing_list.address
+        self.queue = []
+        self.archive = self.mailing_list.config.get('archive',
+                                                    config.storage_path)
+        
+    def get_raw_email():
+        try:        
+            raw_email = sys.stdin.read()
+        except:
+            raise IOError('Can not get a valid email from stdin')
+        return raw_email
+
+    def save_raw_email():
+        filename = os.path.join(self.archive,
+                                datetime.today().strftime('%Y%d%m%H%M%S%f'))
+        tmpfile = file(filename, 'w')
+        tmpfile.write(raw_email)
+        tmpfile.close()
+        self.queue.append(filename)
+
+    def send_email():
+        if not self.queue:
+            raise ValueError('The emails queue is empty')
+        next_email = self.queue.pop()
+        email_file = file(next_email, 'r')
+        email_data = email.message_from_file(email_file)
+        email_file.close()
+
+        email_data['Reply-to'] = self.reply_address
+
+        smtp_conn = smtplib.SMTP()
+        smtp_conn.connect()
+        smtp_conn.sendmail(email_data['From'], self.suscriptors,
+                           email_data.as_string())
+        smtp_conn.close()        
Index: postman/storage.py
===================================================================
--- postman/storage.py	(revision 1)
+++ postman/storage.py	(revision 1)
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+import os, json
+
+class JsonStorage():
+
+    """
+    Json-based storage. 
+    """
+
+    def __init__(self, path='storage.json'):
+        if os.path.isdir(path):
+            raise IOError(path, ' is a directory, exiting')
+        self.path = path
+
+    def exists(self):
+        return os.path.exists(self.path)    
+
+    def jsonize(self, obj):
+        """
+        Convert objects to a dictionary of their representation
+        Based on the exmplaes from Doyg Hellmann:
+        http://www.doughellmann.com/PyMOTW/json/#working-with-your-own-types        
+        """
+        jobj = { '__class__':obj.__class__.__name__, 
+                 '__module__':obj.__module__,
+                 }
+        jobj.update(obj.__dict__)
+        return jobj
+
+    def dejsonize(self, jobj):
+        """
+        Convert some data that has been "jsonized" to the original
+        python object. Again, based on the examples from Doug Hellmann
+        """
+        if '__class__' in jobj:
+            class_name = jobj.pop('__class__')
+            module_name = jobj.pop('__module__')
+            module = __import__(module_name)
+            class_ = getattr(module, class_name)
+            args = dict((key.encode('ascii'), value) \
+                        for key, value in jobj.items())
+            return class_(**args)
+        return jobj
+        
+    def write(self, data):
+        with open(self.path, 'w') as storage:
+            json.dump(data, storage, sort_keys=True, indent=4,
+                      default=self.jsonize)
+            return True 
+        return False 
+
+    def read(self):        
+        with open(self.path, 'r') as storage:
+            try:
+                data = json.load(storage,object_hook=self.dejsonize)
+            except ValueError:
+                # if no json data could be imported, the file could be
+                # damaged or perhaps it does not containg json-encoded
+                # data, simply return an empty string
+                #
+                # FIXME: we should notify the user about the problem
+                return ''
+        return data
+
Index: postman/tests/__init__.py
===================================================================
--- postman/tests/__init__.py	(revision 1)
+++ postman/tests/__init__.py	(revision 1)
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+
+from models import *
+
+
Index: postman/tests/models.py
===================================================================
--- postman/tests/models.py	(revision 1)
+++ postman/tests/models.py	(revision 1)
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+from unittest import TestCase
+from postman.models import Member, MailingList
+
+
+class TestMember(TestCase):
+    def setUp(self):
+        self.member = Member('test@example.com')
+
+    def test__validate_address(self):
+        self.assertEqual(self.member._validate_address(self.member.address),
+                         self.member.address)
+        with self.assertRaises(ValueError):
+            self.member._validate_address('notavalidemail')
+
+    
+class TestMailingList(TestCase):
+    def setUp(self):
+        config = {'private': False, 'archive': '/tmp/postman-tests/archive',
+                  'storage': '/tmp/postman-tests/storage'}
+        self.mailing_list = MailingList('test_list', 'test_list@example.com',
+                                        members={}, config=config)
+        self.member =  Member('test@example.com')
+
+    def test__validate_member_object(self):
+        self.assertEqual(self.mailing_list._validate_member_object(self.member),
+                         self.member)
+        with self.assertRaises(TypeError):
+            self.mailing_list._validate_member_object(None)
+            self.mailing_list._validate_member_object('Not a member object')
+            self.mailing_list._validate_member_object(self.mailing_list)
+
+    def test__validate_member(self):
+        # At first the member object is not a member of the list
+        self.assertFalse(self.mailing_list._validate_member(self.member))
+        self.mailing_list.add_member(self.member)
+        self.assertTrue(self.mailing_list._validate_member(self.member))
+        with self.assertRaises(TypeError):
+            self.mailing_list._validate_member(None)
+            self.mailing_list._validate_member('Not a member object')
+            self.mailing_list._validate_member(self.mailing_list)
+        
+    def test__validate_member_by_address(self):
+        self.assertFalse(self.mailing_list._validate_member_by_address(self.member.address))
+        self.mailing_list.add_member(self.member)
+        self.assertTrue(self.mailing_list._validate_member_by_address(self.member.address))
+        with self.assertRaises(ValueError):
+            self.mailing_list._validate_member_by_address(self.member)
+            self.mailing_list._validate_member_by_address(None)            
+            self.mailing_list._validate_member_by_address('Not a member object')
+            self.mailing_list._validate_member_by_address(self.mailing_list)
+
+    def test_members_addresses(self):
+        self.assertEqual(self.mailing_list.members_addresses(), [])
+        self.mailing_list.add_member(self.member)
+        self.assertTrue(self.member.address in \
+                        self.mailing_list.members_addresses())
+
+    def test_add_member(self):
+        self.assertTrue(self.mailing_list.add_member(self.member))
+        self.assertTrue(self.member.address in \
+                        self.mailing_list.members_addresses())
+        # check what happens if the member is already there:
+        self.assertFalse(self.mailing_list.add_member(self.member))
+        # check what happens if we try to add something that is not
+        # a valid Member instance
+        with self.assertRaises(TypeError):
+            self.mailing_list.add_member(None)
+            self.mailing_list.add_member('Not a member object')
+            self.mailing_list.add_member(self.mailing_list)
+
+    def test_add_member_by_address(self):
+        self.assertTrue(self.mailing_list.add_member_by_address(self.member.address))
+        self.assertTrue(self.member.address in \
+                        self.mailing_list.members_addresses())
+        # check what happens if the member is already there:
+        self.assertFalse(self.mailing_list.add_member_by_address(self.member.address))
+        # check what happens if we try to add something that is not
+        # a valid Member instance
+        with self.assertRaises(ValueError):
+            self.mailing_list.add_member_by_address(self.member)
+            self.mailing_list.add_member_by_address(None)            
+            self.mailing_list.add_member_by_address('Not a member object')
+            self.mailing_list.add_member_by_address(self.mailing_list)
+
+    def test_delete_member(self):
+        self.assertFalse(self.mailing_list.delete_member(self.member))
+        self.mailing_list.add_member(self.member)
+        self.assertTrue(self.member.address in \
+                        self.mailing_list.members_addresses())
+        self.assertTrue(self.mailing_list.delete_member(self.member))
+        self.assertFalse(self.member.address in \
+                         self.mailing_list.members_addresses())
+        with self.assertRaises(TypeError):
+            self.mailing_list.delete_member(None)
+            self.mailing_list.delete_member('Not a member object')
+            self.mailing_list.delete_member(self.mailing_list)
Index: postman/tools.py
===================================================================
--- postman/tools.py	(revision 1)
+++ postman/tools.py	(revision 1)
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+import re
+
+def validate_email_address(address):
+
+    """
+    This function validates a given address, returning True
+    if it is a valid address, False otherwise.
+
+    The function uses a regexp from the django project, and
+    it is inspired in this snippet:
+
+    http://djangosnippets.org/snippets/1093/
+    """
+
+    if not isinstance(address, str):
+        return False
+
+    email_re = re.compile(
+    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
+    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
+    r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE)  # domain
+    return True if email_re.match(address) else False
Index: c/config.py
===================================================================
--- src/config.py	(revision 0)
+++ 	(revision )
@@ -1,10 +1,0 @@
-# -*- coding: utf-8 -*-
-
-import os
-
-# The default path for storage files
-storage_path = os.path.join(os.path.dirname(__file__), 'db')
-
-# Set to True to set that, by default, only emails from members
-# will go into the list
-private_mailing = True
Index: c/models.py
===================================================================
--- src/models.py	(revision 0)
+++ 	(revision )
@@ -1,106 +1,0 @@
-# -*- coding: utf-8 -*-
-
-import os
-from tools import validate_email_address
-from storage import JsonStorage
-import config
-
-class Member():
-
-    """
-    Class that defines a mailing list member
-    """
-
-    def __init__(self, address):
-        self.address = self._validate_address(address)
-        
-    def __repr__(self):
-        return "<Member '%s'>" % self.address
-
-    def __str__(self):
-        return self.address
-
-    def _validate_address(self, address):
-        if not validate_email_address(address):
-            raise ValueError(address, ' is not a valid email address')
-        return address
-
-
-class MailingList():
-
-    """
-    Class that defines a mailing list
-    """
-
-    def __init__(self, name, address, members={}, config={}, storage=None):
-        self.name = name
-        self.address = address
-        self.members = members
-        self.config = config
-        # FIXME: The path to the storage files directory should be defined
-        # in a configuration file
-        self.storage = JsonStorage(os.path.join(config.storage_path,
-                                                self.address))
-
-    def __repr__(self):
-        return "<MailingList '%s'>" % self.address
-
-    def __str__(self):
-        return self.address
-
-    def _validate_config(self):
-        if not 'storage' in self.config.keys():
-            self.config['storage'] = os.path.join(config.storage_path,
-                                                  self.address)
-        if not 'archive' in self.config.keys():
-            self.config['archive'] = os.path.join(self.config['storage'],
-                                                  'archive')
-        if not 'private' in self.config.keys():
-            self.config['private'] = config.private
-        
-    def _validate_member_object(self, member):
-        if not isinstance(member, Member):
-            raise TypeError(member, ' is not a valid Member instance')
-        return member
-
-    def _validate_member(self, member):
-        member = self._validate_member_object(member)
-        return member.address in self.members_addresses()
-
-    def _validate_member_by_address(self, address):
-        if not validate_email_address(address):
-            raise ValueError(address, ' is not a valid email address')
-        return address in self.members_addresses()
-
-    def members_addresses(self):
-        return self.members.keys()
-
-    def add_member(self, member):
-        member = self._validate_member_object(member)
-        if self._validate_member(member):
-            return False
-        self.members[member.address] = member
-        return True
-
-    def add_member_by_address(self, address):
-        if self._validate_member_by_address(address):
-            return False
-        member = Member(address)
-        self.members[address] = member
-                
-    def delete_member(self, member):
-        member = self._validate_member_object(member)
-        if not self._validate_member(member):
-            return False
-        del self.members[member.address]
-        return True
-
-    def load(self):
-        if self.storage.exists():
-            data = self.storage.read()
-            self.name = data.name
-            self.address = data.address
-            self.members = data.members
-    
-    def save(self):
-        self.storage.write(self)
Index: c/mta.py
===================================================================
--- src/mta.py	(revision 0)
+++ 	(revision )
@@ -1,49 +1,0 @@
-# -*- coding: utf-8 -*-
-
-import sys, email, smtplib
-from datetime import datetime
-from models import MailingList
-import config
-
-class Sendmail():
-    
-    def __init__(self, mailing_list):
-        if not isinstance(mailing_list, MailingList):
-            raise ValueError(mailing_list, ' is not a valid mailing list')
-        self.mailing_list = mailing_list
-        self.suscriptors = self.mailing_list.members_addresses
-        self.reply_address = self.mailing_list.address
-        self.queue = []
-        self.archive = self.mailing_list.config.get('archive',
-                                                    config.storage_path)
-        
-    def get_raw_email():
-        try:        
-            raw_email = sys.stdin.read()
-        except:
-            raise IOError('Can not get a valid email from stdin')
-        return raw_email
-
-    def save_raw_email():
-        filename = os.path.join(self.archive,
-                                datetime.today().strftime('%Y%d%m%H%M%S%f'))
-        tmpfile = file(filename, 'w')
-        tmpfile.write(raw_email)
-        tmpfile.close()
-        self.queue.append(filename)
-
-    def send_email():
-        if not self.queue:
-            raise ValueError('The emails queue is empty')
-        next_email = self.queue.pop()
-        email_file = file(next_email, 'r')
-        email_data = email.message_from_file(email_file)
-        email_file.close()
-
-        email_data['Reply-to'] = self.reply_address
-
-        smtp_conn = smtplib.SMTP()
-        smtp_conn.connect()
-        smtp_conn.sendmail(email_data['From'], self.suscriptors,
-                           email_data.as_string())
-        smtp_conn.close()        
Index: c/storage.py
===================================================================
--- src/storage.py	(revision 0)
+++ 	(revision )
@@ -1,65 +1,0 @@
-# -*- coding: utf-8 -*-
-
-import os, json
-
-class JsonStorage():
-
-    """
-    Json-based storage. 
-    """
-
-    def __init__(self, path='storage.json'):
-        if os.path.isdir(path):
-            raise IOError(path, ' is a directory, exiting')
-        self.path = path
-
-    def exists(self):
-        return os.path.exists(self.path)    
-
-    def jsonize(self, obj):
-        """
-        Convert objects to a dictionary of their representation
-        Based on the exmplaes from Doyg Hellmann:
-        http://www.doughellmann.com/PyMOTW/json/#working-with-your-own-types        
-        """
-        jobj = { '__class__':obj.__class__.__name__, 
-                 '__module__':obj.__module__,
-                 }
-        jobj.update(obj.__dict__)
-        return jobj
-
-    def dejsonize(self, jobj):
-        """
-        Convert some data that has been "jsonized" to the original
-        python object. Again, based on the examples from Doug Hellmann
-        """
-        if '__class__' in jobj:
-            class_name = jobj.pop('__class__')
-            module_name = jobj.pop('__module__')
-            module = __import__(module_name)
-            class_ = getattr(module, class_name)
-            args = dict((key.encode('ascii'), value) \
-                        for key, value in jobj.items())
-            return class_(**args)
-        return jobj
-        
-    def write(self, data):
-        with open(self.path, 'w') as storage:
-            json.dump(data, storage, sort_keys=True, indent=4,
-                      default=self.jsonize)
-            return True 
-        return False 
-
-    def read(self):        
-        with open(self.path, 'r') as storage:
-            try:
-                data = json.load(storage,object_hook=self.dejsonize)
-            except ValueError:
-                # if no json data could be imported, the file could be
-                # damaged or perhaps it does not containg json-encoded
-                # data, simply return an empty string
-                #
-                # FIXME: we should notify the user about the problem
-                return ''
-        return data
-
Index: c/tools.py
===================================================================
--- src/tools.py	(revision 0)
+++ 	(revision )
@@ -1,21 +1,0 @@
-# -*- coding: utf-8 -*-
-
-import re
-
-def validate_email_address(address):
-
-    """
-    This function validates a given address, returning True
-    if it is a valid address, False otherwise.
-
-    The function uses a regexp from the django project, and
-    it is inspired in this snippet:
-
-    http://djangosnippets.org/snippets/1093/
-    """
-
-    email_re = re.compile(
-    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
-    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
-    r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE)  # domain
-    return True if email_re.match(address) else False
