From 8bb753e89003309a67370419a9c5cb6c0f9bf28b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 27 Sep 2013 20:10:47 +0200 Subject: [PATCH] SQLite3 support. fixes #484 --- README.rst | 4 +- doc/integration/imap_and_smtp.rst | 39 ++++++--- modoboa/core/commands/postfix_maps.py | 111 +++++++++++++++++--------- 3 files changed, 106 insertions(+), 48 deletions(-) diff --git a/README.rst b/README.rst index 7c17f005f..d98c1dd09 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,8 @@ as an administration panel or a webmail. Modoboa integrates with well known software such as `Postfix `_ or `Dovecot `_. A SQL database (`MySQL `_, `PostgreSQL -`_) is used as a central point of -communication between all components. +`_ or `SQLite `_) +is used as a central point of communication between all components. Modoboa is developed with modularity in mind, expanding it is really easy. Actually, all current features are extensions. diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index 3838c29ba..e2c28e187 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -161,6 +161,23 @@ PostgreSQL users iterate_query = SELECT email AS username FROM admin_user +SQLite users +------------ + +:: + + driver = sqlite + + connect = + + default_pass_scheme = CRYPT + + password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1 + + user_query = SELECT '/%d/%n' AS home, as uid, as gid, ('*:bytes=' || mb.quota || 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' + + iterate_query = SELECT email AS username FROM admin_user + .. note:: Replace values between ``<>`` with yours. @@ -228,6 +245,8 @@ Finally, edit the :file:`dovecot-dict-sql.conf.ext` file and put the following content inside:: connect = host= dbname= user= password= + # SQLite users + # connect = /path/to/the/database.db map { pattern = priv/quota/storage @@ -404,7 +423,7 @@ be used by Postfix to lookup into Modoboa tables. To automaticaly generate the requested map files and store them in a directory, run the following command:: - $ modoboa-admin.py postfix_maps --dbtype mapfiles + $ modoboa-admin.py postfix_maps --dbtype mapfiles :file:`mapfiles` is the directory where the files will be stored. Answer the few questions and you're done. @@ -427,22 +446,24 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file virtual_mailbox_base = /var/vmail relay_domains = - virtual_mailbox_domains = mysql:/etc/postfix/sql-domains.cf - virtual_alias_domains = mysql:/etc/postfix/sql-domain-aliases.cf - virtual_mailbox_maps = mysql:/etc/postfix/sql-mailboxes.cf - virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf, - mysql:/etc/postfix/sql-domain-aliases-mailboxes.cf, - mysql:/etc/postfix/sql-email2email.cf, - mysql:/etc/postfix/sql-catchall-aliases.cf + virtual_mailbox_domains = :/etc/postfix/sql-domains.cf + virtual_alias_domains = :/etc/postfix/sql-domain-aliases.cf + virtual_mailbox_maps = :/etc/postfix/sql-mailboxes.cf + virtual_alias_maps = :/etc/postfix/sql-aliases.cf, + :/etc/postfix/sql-domain-aliases-mailboxes.cf, + :/etc/postfix/sql-email2email.cf, + :/etc/postfix/sql-catchall-aliases.cf smtpd_recipient_restrictions = ... - check_recipient_access mysql:/etc/postfix/maps/sql-maintain.cf + check_recipient_access :/etc/postfix/maps/sql-maintain.cf permit_mynetworks ... # Stuff after +Replace ```` by the name of the database you use. + Then, edit the :file:`/etc/postfix/master.cf` file and add the following definition at the end:: diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index afe3b57d5..e8c0e549a 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -6,69 +6,111 @@ from django.template import Context, Template from modoboa.core.management import Command -class MapFile(object): - category = 'std' +class MapFilesGenerator(object): def __init__(self): - self.base_tpl = Template("""user = {{ dbuser }} -password = {{ dbpassword }} + self.template = Template("""user = {{ dbuser }} +password = {{ dbpass }} dbname = {{ dbname }} hosts = {{ dbhost }} query = {{ query|safe }} """) + self.tpl_context = { + 'dbhost': raw_input('Database host: '), + 'dbname': raw_input('Database name: '), + 'dbuser': raw_input('Username: '), + 'dbpass': getpass.getpass('Password: ') + } + if not self.tpl_context['dbhost']: + self.tpl_context['dbhost'] = '127.0.0.1' - def render(self, destdir, dbtype, **options): + def render(self, args): + """Render all map files. + + :param args: arguments received from the command line + """ try: - options['query'] = getattr(self, dbtype) - except AttributeError: - return + os.mkdir(args.destdir) + except OSError: + pass - content = self.base_tpl.render(Context(options)) - fp = open("%s/%s" % (destdir, self.filename), "w") - fp.write(content) - fp.close() + for v in globals().values(): + if not inspect.isclass(v) or not issubclass(v, MapFile): + continue + if v is MapFile: + continue + if not v.category in args.categories: + continue + content = self.template.render( + Context(dict(self.tpl_context.items(), + query=getattr(v, args.dbtype))) + ) + with open("%s/%s" % (args.destdir, v.filename), "w") as fp: + print >> fp, content + + +class SQLiteMapFilesGenerator(MapFilesGenerator): + + def __init__(self): + self.template = Template("""dbpath = {{ dbpath }} +query = {{ query|safe }} +""") + self.tpl_context = { + 'dbpath': raw_input('Database path: ') + } + + +class MapFile(object): + category = 'std' class DomainsMap(MapFile): filename = 'sql-domains.cf' mysql = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" postgres = "SELECT name FROM admin_domain WHERE name='%s' AND enabled" + sqlite = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" class DomainsAliasesMap(MapFile): filename = 'sql-domain-aliases.cf' mysql = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" postgres = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled AND dom.enabled" + sqlite = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" class MailboxesMap(MapFile): filename = 'sql-mailboxes.cf' mysql = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" postgres = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user u ON mb.user_id=u.id WHERE dom.enabled AND dom.name='%d' AND u.is_active AND mb.address='%u'" + sqlite = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" class AliasesMap(MapFile): filename = 'sql-aliases.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT concat(al.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled AND al.extmboxes<>'')" + sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT (al.address || '@' || dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" class DomainAliasesMailboxesMap(MapFile): filename = 'sql-domain-aliases-mailboxes.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" + sqlite = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" class CatchallAliasesMap(MapFile): filename = 'sql-catchall-aliases.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" + sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" class MaintainMap(MapFile): filename = 'sql-maintain.cf' mysql = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" postgres = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + sqlite = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" class TransportMap(MapFile): @@ -76,6 +118,7 @@ class TransportMap(MapFile): filename = 'sql-transport.cf' mysql = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" postgres = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" + sqlite = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" class AutoRepliesMap(MapFile): @@ -83,6 +126,7 @@ class AutoRepliesMap(MapFile): filename = 'sql-autoreplies.cf' mysql = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" postgres = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" + sqlite = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" class PostfixMapsCommand(Command): @@ -90,30 +134,23 @@ class PostfixMapsCommand(Command): def __init__(self, *args, **kwargs): super(PostfixMapsCommand, self).__init__(*args, **kwargs) - self._parser.add_argument('destdir', type=str, - help='Directory that will contain generated map files') - self._parser.add_argument('--dbtype', type=str, choices=['mysql', 'postgres'], default='mysql', - help='Used database type') - self._parser.add_argument('--dbhost', type=str, default='127.0.0.1', - help='Database host address') - self._parser.add_argument('--categories', type=str, nargs='*', default=['std'], - help='Map file categories to generate (choices: std, autoreply)') + self._parser.add_argument( + 'destdir', type=str, + help='Directory that will contain generated map files' + ) + self._parser.add_argument( + '--dbtype', type=str, choices=['mysql', 'postgres', 'sqlite'], + default='mysql', + help='Used database type' + ) + self._parser.add_argument( + '--categories', type=str, nargs='*', default=['std'], + help='Map file categories to generate (choices: std, autoreply)' + ) def handle(self, parsed_args): - dbname = raw_input('Database name: ') - dbuser = raw_input('Username: ') - dbpass = getpass.getpass('Password: ') - - try: - os.mkdir(parsed_args.destdir) - except OSError: - pass - - for k, v in globals().iteritems(): - if not inspect.isclass(v) or not issubclass(v, MapFile): - continue - if not v.category in parsed_args.categories: - continue - v().render(parsed_args.destdir, parsed_args.dbtype, - dbname=dbname, dbuser=dbuser, dbpassword=dbpass, - dbhost=parsed_args.dbhost) + if parsed_args.dbtype == 'sqlite': + g = SQLiteMapFilesGenerator() + else: + g = MapFilesGenerator() + g.render(parsed_args)