Benutzer-Werkzeuge

Webseiten-Werkzeuge


mail:getmail_ldap

Getmail mit LDAP als Backend

Dieses Tutorial basiert auf dem von Markus Effinger. Das ursprüngliche Tutorial ist Teil einer Serie, die die Einrichtung von Dovecot in Verbindung mit Exim, OpenLDAP und Getmail zum Abrufen von externen Konten zeigt. Dies habe ich angepasst, sodass es in einem mit iRedMail (wichtigster Unterschied Postfix) aufgesetzten System funktioniert und noch etwas verfeinert.

LDAP Anpassungen

LDAP Schema integrieren

Für den Abruf werden weitere Felder und Attribute im LDAP benötigt, folgendes muss als getmail.schema in /etc/ldap/schema gespeichert werden:

/etc/ldap/schema/getmail.schema
# getmail ldapv3 directory schema
#
# LDAP Schema for Getmail, idea originally from http://www.effinger.org
#
# Last change: July 1, 2010
#
# General guideline:
# 1. The language in this file is english
# 2. Every OID in this file must look like this: ns.a.b.c.d, where
#    ns - the official namespace of the Host-Consultants schema:
#         1.3.6.1.4.1.36000
#    a  - Reserved, must always be 1 for the dovecot scheme.
#    b  - ID of object class - e.g.
#         1 = dcMailUser
#         2 = getmailExternalMailAccount
#         3 = dcMailAlias
#         4 = getmailPosixSubAccount
#    c  - Entry type (1:attribute, 2:object)
#    d  - Serial number (increased with every new entry)
# 3. Every entry in this file MUST have a "DESC" field, containing a
#    suitable description!
# 4. New entries are to be kept as generic as possible.
# 5. Attributes are listed in front of objects. All entries must be
#    ordered by their serial number.
#
# This schema depends on:
# - core.schema
# - cosine.schema
# - nis.schema
#
 
# Attribute Type Definitions
attributetype ( 1.3.6.1.4.1.36000.1.2.1.1
	NAME 'getmailDeliverTo'
	DESC 'An adress to which the fetched mails get delivered'
	EQUALITY caseIgnoreIA5Match
	SUBSTR caseIgnoreIA5SubstringsMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
 
attributetype ( 1.3.6.1.4.1.36000.1.2.1.2
	NAME 'getmailAccountStatus'
	DESC 'The status of a user account: active, noaccess, disabled, deleted'
	EQUALITY caseIgnoreIA5Match
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
	SINGLE-VALUE )
 
attributetype ( 1.3.6.1.4.1.36000.1.2.1.3
	NAME 'getmailRetrieveType'
	DESC 'Tells getmail what mail account to retrieve mail from, and how to access that account, e.g. SimplePOP3Retriever and BrokenUIDLPOP3SSLRetriever' 
	EQUALITY caseIgnoreIA5Match 
	SUBSTR caseIgnoreIA5SubstringsMatch 
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
 
attributetype ( 1.3.6.1.4.1.36000.1.2.1.4
	NAME 'getmailRetrieveServer' 
	DESC 'Incoming mails have to be downloaded from this server' 
	EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch 
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
 
attributetype ( 1.3.6.1.4.1.36000.1.2.1.5
	NAME 'getmailRetrieveLogin' 
	DESC 'Login credential to receive Mail from the server' 
	EQUALITY caseExactIA5Match 
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
 
attributetype ( 1.3.6.1.4.1.36000.1.2.1.6
	NAME 'getmailRetrievePassword' 
	DESC 'Password for mail retrieval in clear text' 
	EQUALITY caseExactIA5Match 
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
 
attributetype ( 1.3.6.1.4.1.36000.1.3.1.1
	NAME 'getmailPosixOwnerURL'
		DESC 'Identifies an URL associated with the posixOwner of the entry. Any type of labeled URL can be used.'
		SUP labeledURI )
 
 
# Object Class Definitions
 
objectclass ( 1.3.6.1.4.1.36000.1.2.2.1
	NAME 'getmailExternalMailAccount'
	DESC 'Dovecot-LDAP external mail account'
	SUP top STRUCTURAL MUST ( getmailDeliverTo $ getmailAccountStatus )
	MAY ( getmailRetrieveType $ getmailRetrieveServer $ getmailRetrieveLogin $ getmailRetrievePassword ) )
 
objectclass ( 1.3.6.1.4.1.36000.1.3.2.2
	NAME 'getmailPosixSubAccount'
	DESC 'LDAP-URL for retrieving the respective posixAccount of an entry'
	SUP top AUXILIARY MAY getmailPosixOwnerURL )

der slapd.conf die neue Konfiguration bekannt machen

nano /etc/ldap/slapd.conf
 
include /etc/ldap/schema/getmail.schema

Gruppe im LDAP für Getmail anlegen

Ich habe mich dafür entschieden Konten zum Abruf in der ou Getmail abzulegen, daher habe ich diese im LDAP angelegt

dn: ou=Getmail,domainName=host-consultants.de,o=domains,dc=host-consultants,dc=de
objectclass: organizationalUnit
objectclass: top
ou: Getmail

Ersten Benutzer zum Abruf anlegen

Beispiel LDIF für einen Abrufbenutzer:

dn: dcSubMailAddress=test1@host-consultants.de,ou=Getmail,domainName=host-consultants.de,o=domains,dc=host-consultants,dc=de
dcaccountstatus: active
dcretrievelogin: fetchtest@host-consultants.de
dcretrievepassword: PaSsWoRt
dcretrieveserver: mail.host-consultants.de
dcretrievetype: SimplePOP3Retriever
dcsubmailaddress: test1@host-consultants.de
objectclass: dcExternalMailAccount
objectclass: dcPosixSubAccount
objectclass: top

Skript zum Abruf

Installation der benötigten Pakete

aptitude install getmail4 python-ldap

Benutzer anlegen & Skript

Es ist zu empfehlen für den Abruf der E-Mails einen eigenen Benutzer anzulegen ( != ausführung als Root ).

adduser secmail

anschließend wechseln wir direkt zu dem neuen Benutzer:

su secmail

Direkt im Hauptverzeichnis des Benutzers wird dann folgendes Skript abgelegt

/home/secmail/qetmail-ldap.py
#!/usr/bin/python
# File: getmail-ldap.py
try:
	import errno
	import string
	import logging
	import logging.handlers
	import ldap
	import ConfigParser
	import ldif
	import threading
	from StringIO import StringIO
	from ldap.cidict import cidict
	from os.path import os
	from subprocess import Popen,PIPE
except ImportError:
	print """Cannot find all required libraries please install them and try again"""
	raise SystemExit
 
config_file_location = '/home/secmail/getmail-ldap.cfg'
 
def pid_exists(pid):
    """Is there a process with PID pid?"""
    if pid < 0:
        return False
 
    exist = False
    try:
        os.kill(pid, 0)
        exist = 1
    except OSError, x:
        if x.errno != errno.ESRCH:
            raise
 
    return exist
 
def get_search_results(results):
    """Given a set of results, return a list of LDAPSearchResult
    objects.
    """
    res = []
 
    if type(results) == tuple and len(results) == 2 :
        (code, arr) = results
    elif type(results) == list:
        arr = results
 
    if len(results) == 0:
        return res
 
    for item in arr:
        res.append( LDAPSearchResult(item) )
 
    return res
 
class LDAPSearchResult:
    """A class to model LDAP results.
    """
 
    dn = ''
 
    def __init__(self, entry_tuple):
        """Create a new LDAPSearchResult object."""
        (dn, attrs) = entry_tuple
        if dn:
            self.dn = dn
        else:
            return
 
        self.attrs = cidict(attrs)
 
    def get_attributes(self):
        """Get a dictionary of all attributes.
        get_attributes()->{'name1':['value1','value2',...],
				'name2: [value1...]}
        """
        return self.attrs
 
    def set_attributes(self, attr_dict):
        """Set the list of attributes for this record.
 
        The format of the dictionary should be string key, list of
        string alues. e.g. {'cn': ['M Butcher','Matt Butcher']}
 
        set_attributes(attr_dictionary)
        """
 
        self.attrs = cidict(attr_dict)
 
    def has_attribute(self, attr_name):
        """Returns true if there is an attribute by this name in the
        record.
 
        has_attribute(string attr_name)->boolean
        """
        return self.attrs.has_key( attr_name )
 
    def get_attr_values(self, key):
        """Get a list of attribute values.
        get_attr_values(string key)->['value1','value2']
        """
        return self.attrs[key]
 
    def get_attr_names(self):
        """Get a list of attribute names.
        get_attr_names()->['name1','name2',...]
        """
        return self.attrs.keys()
 
    def get_dn(self):
        """Get the DN string for the record.
        get_dn()->string dn
        """
        return self.dn
 
    def pretty_print(self):
        """Create a nice string representation of this object.
 
        pretty_print()->string
        """
        str = "DN: " + self.dn + "\n"
        for a, v_list in self.attrs.iteritems():
            str = str + "Name: " + a + "\n"
            for v in v_list:
                str = str + "  Value: " + v + "\n"
        str = str + "========"
        return str
 
    def to_ldif(self):
        """Get an LDIF representation of this record.
 
        to_ldif()->string
        """
        out = StringIO()
        ldif_out = ldif.LDIFWriter(out)
        ldif_out.unparse(self.dn, self.attrs)
        return out.getvalue()
 
class RetrieveMails(threading.Thread):
	def __init__(self, getmail_binary, config_filename, config_data_dir):
		threading.Thread.__init__(self)
		self.getmail_binary, self.config_filename, self.config_data_dir = \
			getmail_binary, config_filename, config_data_dir
	def run(self):
		try:
			command = [self.getmail_binary, \
				#'--quiet', \
				'--rcfile=' + self.config_filename, \
				'--getmaildir=' + self.config_data_dir]
			self.pid_filename = self.config_filename + '.pid'
			# Check for a pidfile to see if the daemon already runs
			try:
				pid_file = file(self.pid_filename,'r')
				pid_number = pid = int(pid_file.read().strip())
				pid_file.close()
			except IOError:
				pid = None
			# Check whether process is really running
			if pid:
				pid = pid_exists(pid)
			if not pid:
				getmail_process = Popen(command, shell=False,stdout=PIPE,stderr=PIPE)
				try:
					file(self.pid_filename,'w+').write("%s\n" % getmail_process.pid)
					getmail_process.wait()
				finally:
					os.remove(self.pid_filename)
					# Zur Sicherheit die erstellte Konfigurationsdatei loeschen (Login-Daten!)
					os.remove(self.config_filename)
				stderr_output=string.join(getmail_process.stderr.readlines())
				if getmail_process.returncode <> 0 or len(stderr_output.strip())>0 :
					raise Exception, "Getmail command failed for " + " ".join(command) \
						+"\nStdErr: \n" + string.join(stderr_output.strip()) \
						+"\nStdOut: \n" + string.join(getmail_process.stdout.readlines())
			else:
				log_object.info("Command " + " ".join(command) +\
					" not executed, existing pid " + str(pid_number) + " found")
		except:
			log_object.exception("An error occured!")
 
class RetrieveAccount:
	account_name = None
	account_type = None
	login = None
	password = None
	server = None
	def __init__(self, account_name=None, account_type=None, server=None, login=None, password=None):
		self.account_name, self.account_type, self.login, self.password, self.server = \
			account_name, account_type, login, password, server
 
class GetmailConfigFile(ConfigParser.SafeConfigParser):
	output_filename = None
	def __init__(self, defaults, default_config_filename=None, output_filename=None):
		ConfigParser.SafeConfigParser.__init__(self, defaults)
		if default_config_filename is not None:
			self.read(default_config_filename)
		self.output_filename = output_filename
	def set_pop3_account(self, newRetrieveAccount):
		self.set('retriever','server',newRetrieveAccount.server)
		self.set('retriever','type',newRetrieveAccount.account_type)
		self.set('retriever','username',newRetrieveAccount.login)
		self.set('retriever','password',newRetrieveAccount.password)
		self.set('destination','arguments','("'+newRetrieveAccount.account_name+'",)')
	def write(self):
		if self.output_filename is not None:
			"""try:
				output_file = open(self.output_filename, 'wb')
			except:
				raise Exception, "Unable to open " + \
					self.output_filename + "for writing"
			finally:
				output_file.close()
			"""
			os.umask(0077)
			output_file = open(self.output_filename, 'wb')
			ConfigParser.SafeConfigParser.write(self, output_file)
		else:
			raise Exception, "No output file for configuration defined"
 
# Konfigurationsdatei lesen
config_object = ConfigParser.SafeConfigParser()
config_object.read(config_file_location)
 
# Set-up Logging
log_object = logging.getLogger("getmail-ldap")
log_object.setLevel(logging.DEBUG)
 
# This handler writes everything to a log file.
log_file_handler = logging.FileHandler(config_object.get('Logging','LogFile'))
log_file_formatter = logging.Formatter("%(levelname)s %(asctime)s %(funcName)s %(lineno)d %(message)s")
log_file_handler.setFormatter(log_file_formatter)
log_file_handler.setLevel(logging.DEBUG)
log_object.addHandler(log_file_handler)
 
# This handler emails anything that is an error or worse.
log_smtp_handler = logging.handlers.SMTPHandler(\
	config_object.get('Logging','MailServer'),\
	config_object.get('Logging','MailFrom'),\
	config_object.get('Logging','MailTo').split(','),\
	config_object.get('Logging','MailSubject'))
log_smtp_handler.setLevel(logging.ERROR)
log_smtp_handler.setFormatter(log_file_formatter)
log_object.addHandler(log_smtp_handler)
 
def main_call():
 
	## first you must open a connection to the LDAP server
	ldap_object = ldap.open(config_object.get('LDAP','LDAPServer'))
	ldap_object.simple_bind_s(\
		config_object.get('LDAP','BindDN'),\
		config_object.get('LDAP','BindPassword'))
	# searching doesn't require a bind in LDAP V3.
	# If you're using LDAP v2, set the next line appropriately
	# and do a bind as shown in the above example.
	# you can also set this to ldap.VERSION2 if you're using a v2 directory
	# you should  set the next option to ldap.VERSION2 if you're using a v2 directory
	ldap_object.protocol_version = ldap.VERSION3	
 
	## The next lines will also need to be changed to support your search requirements and directory
	## retrieve all attributes - again adjust to your needs - see documentation for more options
 
	if config_object.get('LDAP','SearchScope').upper() == "SUB":
            search_scope = ldap.SCOPE_SUBTREE
        elif config_object.get('LDAP','SearchScope').upper() == "ONE":
            search_scope = ldap.SCOPE_ONELEVEL
        else:
            search_scope = ldap.SCOPE_BASE
 
	ldap_result_id = ldap_object.search( \
		config_object.get('LDAP','SearchDN'), \
		search_scope,
		config_object.get('LDAP','SearchFilter'), \
		None)
 
	ldap_results = []
 
	while 1:
		result_type, result_data = ldap_object.result(ldap_result_id, 0)
		if (result_data == []):
			break
		else:
			## here you don't have to append to a list
			## you could do whatever you want with the individual entry
			## The appending to list is just for illustration.
			if result_type == ldap.RES_SEARCH_ENTRY:
				ldap_results += get_search_results(result_data)
	for ldap_result in ldap_results:
		account = RetrieveAccount( \
			# Account Name \
			ldap_result.get_attr_values(\
				config_object.get('LDAP','RelevantAttributes').split(',')[0])[0] ,\
			# Account Type \
			ldap_result.get_attr_values(\
				config_object.get('LDAP','RelevantAttributes').split(',')[1])[0],\
			# Server \
			ldap_result.get_attr_values(\
				config_object.get('LDAP','RelevantAttributes').split(',')[2])[0],\
			# Login \
			ldap_result.get_attr_values(\
				config_object.get('LDAP','RelevantAttributes').split(',')[3])[0],\
			# Password \
			ldap_result.get_attr_values(\
				config_object.get('LDAP','RelevantAttributes').split(',')[4])[0]\
		)
		config_output_filename = os.path.join(\
			config_object.get('Main','ConfigFileOutputDir'), \
			"getmail_" + \
			account.account_name + \
			".cfg")
		config_file = GetmailConfigFile(None, \
			config_object.get('Main','DefaultGetmailConfigFile'), config_output_filename)
		config_file.set_pop3_account(account)
		log_object.info("Writing Account Configuration for " + account.account_name + \
				" to file " + config_output_filename)
		config_file.write()
		RetrieveMails(\
			config_object.get('Main','GetmailBinary'), \
			config_output_filename, \
			config_object.get('Main','GetmailDir')\
		).start()
		#print config_output_filename
		#print "Name " + account.account_name
		#print "Type " + account.account_type
		#print "Server " + account.server
		#print "Login " + account.login
		#print "Password " + account.password
		#print "-----------------"
		#print ldap_result.pretty_print()
 
if __name__ == "__main__":
	try:
		main_call();
	except:
		log_object.exception("An error occured!")

Dann nur noch das Skript ausführbar machen

chmod 750 getmail-ldap.py

Konfigurationsdatei für getmail-ldap.py

Das Phyton Skript hat eine eigene Konfigurationsdatei, diese wird unter /home/secmail/getmail-ldap.cfg abgelegt.

/home/secmail/getmail-ldap.cfg
[Main]
# Path to getmail
GetmailBinary=/usr/bin/getmail
# Directory that should be used as a storage by getmail
GetmailDir=/home/secmail/getmail_data
# Read default values for getmail from this file
DefaultGetmailConfigFile=/home/secmail/getmailrc_template.cfg
# Save the final configuration files which include the LDAP details to this directory
ConfigFileOutputDir=/home/secmail/getmail_config
 
[Logging]
# Write messages to the following log file
LogFile=/var/log/getmail-ldap.log
# If a severe error occures a mail goes to the admin
# SMTP-Server to use for sending this error notification
MailServer=localhost
# Mail address of the sender of this error notification
MailFrom=secmail@host-consultants.de
# Recipients of this error notification
# separate multiple recipients by comma
MailTo=root@host-consultants.de
# Subject of the error notification
MailSubject=Getmail-LDAP Error
 
[LDAP]
# Read LDAP information from this server
LDAPServer=localhost
# Authenticate with the following DN
BindDN=cn=vmailadmin,dc=host-consultants,dc=de
# Authenticate with the following password
BindPassword=mysecmailpassword
# Restrict search of external mail accounts to this DN
SearchDN=o=domains,dc=host-consultants,dc=de
# Scope of search for external mail accounts
# Possible values include SUB, ONE and BASE
SearchScope=SUB
# Identify external mail accounts with the following filter
SearchFilter=(&(getmailDeliverTo=*)(objectClass=getmailExternalMailAccount)(getmailAccountStatus=active)(getmailRetrieveType=*)(getmailRetrieveLogin=*)(getmailRetrievePassword=*))
# List of LDAP-Attributes used to determine the following variables
# 	1. Name for resulting getmail configuration file (must be unique)
#	2. Type for mail collection e.g. BrokenUIDLPOP3Retriever
#	3. Mail server to collect mails from
#	4. Login for mail server
# 	5. Password for mail server
# separate by comma
RelevantAttributes=getmailDeliverTo,getmailRetrieveType,getmailRetrieveServer,getmailRetrieveLogin,getmailRetrievePassword

Da die Konfigurationsdatei sensible Daten, wie die Zugangsdaten für das LDAP enthält werden die Rechte der Datei so gesetzt das nur der Eigentümer die Datei lesen kann:

chmod 640 getmail-ldap.cfg

Getmail Template

Zum Schluss wird noch ein Template angelegt.

/home/secmail/getmailrc_template.cfg
[retriever]
type =
server =
username =
password = 
 
[destination]
type = MDA_external
path = /usr/sbin/sendmail
arguments = ("user@mailhost.tld",)
 
[options]
# for testing do not delete mails
#delete = false
delete = true
message_log = /var/log/getmail.log
read_all = true
# do not manipulate the header
delivered_to = false
received = false
 
[filter-spamassassin]
type = Filter_external
path = /usr/bin/spamc
arguments = ("--max-size=100000", )
 
[filter-clamav]
type = Filter_classifier
path = /usr/bin/clamdscan
arguments = ("--stdout", "--no-summary", "-")
exitcodes_drop = (1, )

Berechtitungen für das Template setzen

chmod 640 getmailrc_template.cfg

Letzte Dateien anlegen

Zum Schluss müssen noch zwei Arbeitsverzeichnisse für das Skript angelegt werden

mkdir -m 750 /home/secmail/getmail_data /home/secmail/getmail_config

und die Logdateien angelegt und für den Benutzer secmail lesbar gemacht werden (wieder als Root, oder mit Sudo)

touch /var/log/getmail{-ldap,}.log
chown root.secmail /var/log/getmail{-ldap,}.log
chmod 660 /var/log/getmail{-ldap,}.log

Mailabruf testen

Bevor der Skriptabruf per Cron automatisiert werden kann, sollte ein Testlauf gemacht werden. Hierfür wird das Skript (direkt von Root) über folgenden Befehl aufgerufen

sudo -u secmail -s /home/secmail/getmail-ldap.py

Probleme?

  • An welche E-Mail wird lokal zugestellt? → [getmailDeliverTo]
  • Muss ein Alias mit der zuzustellenden Adresse angelegt werden? → Nein, aber die E-Mail-Adresse an die lokal zugestellt werden soll muss existieren. Diese kann aber auch ein Alias sein (Vorteil: so könnte ein Pop Konto an mehrere lokale Empfänger zugestellt werden), oder es muss eine Catch-All Adresse existieren.
  • VORSICHT! Passwörter werden im Klartext in LDAP abgelegt → anonymen Zugriff auf das LDAP verbieten!

Logrotation

/etc/logrotate.d/getmail
# Logrotate Konfiguration für getmail und getmail-ldap
# siehe /home/secmail/getmail-ldap.py
/var/log/getmail.log {
        daily
        missingok
        rotate 14
        compress
        delaycompress
        notifempty
        create 660 root secmail
}
/var/log/getmail-ldap.log {
        daily
        missingok
        rotate 14
        compress
        delaycompress
        notifempty
        create 660 root secmail
}

Regelmäßigen Abruf einrichten

Ein /etc/cron.d/ eine Datei getmail-ldap anlegen mit folgendem Inhalt:

# Retrieve external Mails via getmail
*/10 * * * * secmail /home/secmail/getmail-ldap.py

Anschließend den Cron Daemon neustarten nicht vergessen!

Jetzt sollte per Cron automatisch der Abruf der Mails alle 10 Minuten angestoßen werden.

Referenzen

mail/getmail_ldap.txt · Zuletzt geändert: 2012/02/05 17:07 von fbartels