Qgis Python code tips

At work, I spend some time to develop a qgis extension in order to make Qgis very easy to use. This extension is coded in Python and try to minimise the QGis interface and add two more functionnalities like an interface to choose a city for the current view range and a good radius selection tool (which shows the radius size). Coding this extension was quite easy due to QGis and Python APIs. Much of my time was documentation reading and exemple searches.

Now that the extension is a good prototype, I think I can share some tips I've found for some common problems to solve under QGis. So here are my tips...

Tip 0: Documentation

I guess this is something where every project should start: add some links to the online APIs documentation to your web browser bookmarks. For QGis, you need to point to the API documentation and to PyQt Class Reference because a lot of objects are managed with Qt Bindings.

The QGis API documentation is well designed: the class list is a good start (eg: the gui library). You can find nearly everything you want by starting your search from there.

Tip 1: Howto manage toolbars

When you want to deal with QGis toolbars, you first need to read the QgsInterface and the QToolbar documentation. QgsInterface is a python object from the QGis.gui which manage the QGis Interface that the user interacts with. From this object, you can get access to most of the QtObjects that build the interface. For those tips, you need to learn about the QToolbar objects.

Remove a complete toolbar (eg: the File Toolbar)

# QGis Toolbars are referenced as objects named under the QgsInterface object.
# Consult the QgsInterface documentation to find all the names:
# The object self.iface is a QgsInterface from the start of you plugin.

# First, we get the Toolbar dedicated to File manipulation:
toolbar = self.iface.fileToolBar()

# Then, we get the parent of this toolbar and remove its child.
parent = toolbar.parentWidget()
parent.removeToolBar(toolbar)

Add an action from a toolbar

# You have to define an action (consult QAction doc) and modify or add some of its attributes:
self.selC = QAction(QIcon(":/plugins/permanence/mActionSelectRadius.png"), \
 u"Sélection par cercle", self.iface.mainWindow())
self.selC.setDisabled(True)

# Then, you add this action to the toolbar of your choice:
self.iface.attributesToolBar().addAction(self.selC)

Removing an action from a toolbar

# You have to save all of the actions from the toolbar
actions = self.iface.attributesToolBar().actions()

# then, you clear the complete toolbar
self.iface.attributesToolBar().clear()

# and you re-add only the actions yo uwant
self.iface.attributesToolBar().addAction(actions[3])
self.iface.attributesToolBar().addAction(actions[4])
...
# or some other actions:
self.iface.attributesToolBar().addAction(another_action)

Tip 2: Howto manage menus

Same deal: use QgsInterface to access the menu objects. You need to learn about (Qt) QMenu objects.

Remove a complete menu

You could think that typing: menu.set_visible(False) should do the trick... but it doesn't work. So here is what I found:

 # you have to get a QMenu object from QgsInterface (let's say: file Menu)
 menu = self.iface.fileMenu()
 # Then you get its parent
 menubar = menu.parentWidget()
 # and remove the entire child QMenu QAction:
 menubar.removeAction(menu.menuAction())

Add a complete menu to the extension menu

The extension menu is a QMenu dedicated to the extensions. You can create your own menus and add them to the global menu bar like the menu of fTools. But it is easier to start with the pluginMenu.

# You have to create some QAction which will be the last menu levels:
# A first action:
self.action = QAction(QIcon(":/plugins/permanence/icon.png"), \
        u"Choisir la commune et les thèmes...", self.iface.mainWindow())
QObject.connect(self.action, SIGNAL("triggered()"), self.choix_commune)

# a second one:
self.param = QAction(u"Paramétrages...", self.iface.mainWindow())
QObject.connect(self.param, SIGNAL("triggered()"), self.afficheParam)

# Create an empty menu...
self.menu = QMenu()
self.menu.setTitle(u"&Your specific Menu...")
# Add the actions to this menu:
self.menu.addActions([self.action, self.param])
# Add the menu to the Plugin Menu
self.iface.pluginMenu().addMenu(self.menu)

Tip 3: Howto to load a project file

Loading a project file is something that is very easy... But I've noticed that when you load a project with a lot of layers or groups, it can take minutes to open ! In order to make it faster, you have to disable the rendering during the load and to enable it once the project is opened.

# You start by referencing the actual project instance project = QgsProject.instance()

# Then you disable the rendering operations: self.iface.mapCanvas().setRenderFlag(False) # So you can open the project file faster: projet.read(QFileInfo('/somewhere/the_project.qgs')) # Never forget to re-enable the rendering... self.iface.mapCanvas().setRenderFlag(True)

# At this point, the rendering should be launched.

Tip 4: Howto to manage layers with LegendInterface

From the QgsInterface, you can get a QgsLegendInterface object. This one manages the legend (the list of layers and groups that are shown or not). You can also learn about the layers from the QgsMapLayerRegistry object.

Get the list of layers

# Get the legend object from QgsInterface:
legend = self.iface.legendInterface()
# You can use QgsMapLayerRegistry.instance() object: its mapLayers() object is an iterable dict you can use with for instructions:
layermaps = QgsMapLayerRegistry.instance().mapLayers()
# then you get a dict object: name <--> layer
for name, layer in layermaps.iteritems():
# If you want to get the type of the layer (vector or raster), use:
  if layer.type() == QgsMapLayer.VectorLayer:
# If you want to know if the layer is visible or not user:
  if legend.isLayerVisible(layer):

Get and set visible some groups of layers

# Get the legend and its groups:
legend = self.iface.legendInterface()
groups = legend.groups()

# Loop to enable all the groups:
for i in range(len(groups)):
  legend.setGroupVisible(i+1, True)

Tip 5: Zoom to the extent of an object

In my extension, I made a dialog to choose an object to zoom on. It is the same thing than the Attribute Table "Show selection" tool. But it is much more simple to use. Sometimes, QGis doesn't make the rendering even if the extent of the current view has changed. You have to add a line to be sure that it do it by calling a re-rendering once the new extent is done.

# We have to find the good layer (its name is in the layer_city_name variable):
# This code is much simpler if you already have the QgsLayerMap() object.
layermaps = QgsMapLayerRegistry.instance().mapLayers()
for name, layer in layermaps.iteritems():
   if layer.name() == self.layer_city_name:
              city_layer = layer
# Then we can consider that we have only the name of the city that has been selected.
# We need its extent. This information is simply get from the extent of its geometry.
# So we have to make something like a query to find the city object that matches or name.
provider = city_layer.dataProvider()
feat = QgsFeature()
allAttrs = provider.attributeIndexes()
provider.select(allAttrs)

# Loop on all the name attributes of all of the objects from the layer:
# The first loop is: extract all the objects from the layer:
while provider.nextFeature(feat):
    # fetch map of attributes
    attrs = feat.attributeMap()
    # Second loop: look into the object attributes.
    # Name is the second attribute (so number 1 as attribute count start from 0)
    # in our layer (it could be something else for another layer: just a convention for the example).
    for (k,attr) in attrs.iteritems():
    # the name of the city is kept in city_name variable
      if k == 1 and str(attr.toString()) == str(self.city_name):
        city_extent = feat.geometry().boundingBox()

# With the extent, it is very easy to zoom on the city:
self.iface.mapCanvas().setExtent(city_extent)
# And never forget to refresh the canvas to re-render as said before:
self.iface.mapCanvas().refresh()

Tip 6: Manage rubberband

QgsRubberband objects can be considered as geometrical objects (like any polygon in QGis) on a dedicated and independant (non named) layer for drawing purposes. You can use those objects to deal with selection. When you use a selection object, you draw a geometry (sometime special like a circle) to select the objects that intersects with.

But you have to add some code to start the selection and to change the selection tool before.

Howto Make a circle selection tool with rubberband

(Taken from some piece of code from SelectPlus plugin).

   # Create an action to start the selection process:
   self.selC = QAction(QIcon(":/plugins/permanence/mActionSelectRadius.png"), \
          u"Sélection par cercle", self.iface.mainWindow())
   # Link it to a trigger function (self.selectCircle in our case):
   QObject.connect(self.selC, SIGNAL("triggered()"), self.selectCircle)
   # ... Add this action to a toolbar or a menu like shown above...

   # Here is the trigger function:
   def selectCircle(self):
     # Our tool is an elaborated object that uses QgsRubberband as shown below)
 self.tool = selectTools.selectCircle(self.iface)
 # When this object emits the signal "selectionDone()", it means that the selection is done and that we have to do something (with the do_something function)
 self.iface.connect(self.tool, SIGNAL("selectionDone()"), self.do_something)
 # We indicate to the interface that the tool that is activated is our special selection object.
 self.iface.mapCanvas().setMapTool(self.tool)

   # ... On another python file (called selectTools.py which store ours selection class and functions)...
   # Here is our special object: selectCircle.
   class selectCircle(QgsMapTool):
     # The constructor starts with an empty object:
 def __init__(self,iface, couleur, largeur, cercle):
   canvas = iface.mapCanvas()
   QgsMapTool.__init__(self,canvas)
   self.canvas = canvas
   self.iface = iface
   self.status = 0
   # Number of segments for the circle
   self.cercle = 30
   # Our QgsRubberband:
       self.rb=QgsRubberBand(self.canvas,True)
       self.rb.setColor( Qt.Red )
       self.rb.setWidth( 2 )
       sb = self.iface.mainWindow().statusBar()
       sb.showMessage(u"You are drawing a circle")
       return None

 # When you press the mouse button the following is launched:
 # Actually, it means that we start the drawing work
 def canvasPressEvent(self,e):
   if not e.button() == Qt.LeftButton:
         return
   self.status = 1
   self.center = self.toMapCoordinates(e.pos())
   # The rbcircle function put a circle to the QgsRubberBand geometry:
   # for the moment, the radius is null !
   rbcircle(self.rb, self.center, self.center, self.cercle)
   return

 # When you move your mouse, the following is launched:
 def canvasMoveEvent(self,e):
   # If you are not drawing a circle, nothing appears !
   if not self.status == 1:
         return
   # else, construct a circle with N segments
   cp = self.toMapCoordinates(e.pos())
       rbcircle(self.rb, self.center, cp, self.cercle)
   r = sqrt(self.center.sqrDist(cp))
   # We add the radius and X/Y center coordinates in the status bar:
       sb = self.iface.mainWindow().statusBar()
       sb.showMessage(u"Centre: X=%s Y=%s, RADIUS: %s m" % (str(self.center.x()),str(self.center.y()),str(r)))
       self.rb.show()

 def canvasReleaseEvent(self,e):
       if not e.button() == Qt.LeftButton:
         return
       self.emit( SIGNAL("selectionDone()") )

 def reset(self):
       self.status = 0
       self.rb.reset( True )

 def deactivate(self):
       QgsMapTool.deactivate(self)
   self.emit(SIGNAL("deactivated()"))

 # Here is our circle computation function: an algorithm to build a circle with two points.
 def rbcircle(rb,center,edgePoint,N):
       r = sqrt(center.sqrDist(edgePoint))
       rb.reset( True )
       for itheta in range(N+1):
           theta = itheta*(2.0 * pi/N)
           # You see that only the QgsRubberband geometry is modified
           rb.addPoint(QgsPoint(center.x()+r*cos(theta),center.y()+r*sin(theta)))
       return

Tip 7: Export results to CSV

It is much more a Python tip than a QGis one. But you can use the csv module from Python to export QGis attribute selection to a csv file. But this shows howto extract the attributes of the selection from the layer

   # Preliminaries: import the module, a layer, its data provider, an empty feature, a selection geometry
   import csv
   provider = layer.dataProvider()
   feat = QgsFeature()
   g = a_rubber_band.asGeometry()

   # Then, open a file and make it a csv file object:
   f = open('./a_csv_file,'wb')
   csv_file = csv.writer(f)

   # We want to extract the CSV header line from the layer fields name:
   header = [field.name().toUtf8() for field in provider.fields().values()]
   csv_file.writerow(header)

   # We must verify for all the layer objects that they intersect or not with the previous geometry...
   # So, we scan all the layer object:
   while provider.nextFeature(feat):
     # Get the object geometry
     geom = feat.geometry()
     # and test if it intersects the rubberband geometry (g):
     if geom.intersects(g):
       attrs = feat.attributeMap()
       # Then we extract the current object fields values...
       b = [attr.toString().toUtf8() for attr in attrs.values()]
       # And we export them to our CSV file
       fichier_csv.writerow(b)

   # Don't forget to close the csv file at the end !
   f.close()

Tip 8: Open an independant application from Qgis

It is also more a Python tip than a Qgis one. Sometimes, you want to launch a specific program (like OpenOffice or another independant script) with a (temporary) file you have created from QGis (like a CSV file).

# You want to launch LibreOffice and make it opens a CSV file
# You need to use the subprocess module (from Python 2.6)
import subprocess

# with this call, you open an independant instance of Libreoffice.
# The pid variable can be detroyed without affecting LibreOffice
pid = subprocess.Popen(['libreoffice', csv_file_path]).pid

Conclusion:

I hope that these tips are useful to any Python developper who wants to go further with QGis. From my point of view, the API is really easy to use and well documented. I really believe QGis will become the reference in Desktop GIS !

Posted mar. 03 mai 2011 20:16:40 Tags:

Solution de serveur de courrier électronique auto-hébergé (partie 2) : Débuter avec Exim4 sous Debian.

Introduction:

Exim4 étant le MTA par défaut officiel de Debian, sa configuration a été revue à la sauce Debian. Avant de commencer, il faut bien retenir qu'un MTA n'est pas un logiciel simple à installer. Déjà, le concept du courrier électronique n'est pas trivial car la chaîne "historique" fait intervenir de nombreux agent (voir mon article sur le sujet). Ensuite, un MTA est modulaire: de nombreux éléments sont à configurer pour de nombreuses utilisations différentes. Vous devez gérer l'authentification des utilisateurs, la sécurité et bien évidemment la manière dont vous gérez le courrier électronique: que garder, que redistribuer, que jeter dans /dev/null ?

Néanmoins, si vous vous hébergez et si vous avez envie d'avoir une adresse de courrier électronique rien qu'à vous que vous gérez à 100%, il faut quand même en passer par là.

Avant de commencer, je vous conseille de lire l'introduction à Exim4 de GNU/Linux Magazine France Hors-Série n°36. Ensuite, vous pouvez lire le fichier /usr/share/doc/exim4/README.Debian.gz (mais il faut installer exim4 avant de le lire). Enfin, la spécification de Exim4 située dans /usr/share/doc/exim4/spec.txt.gz sera d'un grand secours une fois que vous aurez envie d'aller plus loin ou de comprendre en détail ce que vous écrivez dans la configuration.

L'objectif de cette série d'articles est de monter un MTA sur votre serveur de manière à disposer d'un vrai service de courrier électronique sur Internet. A l'issue de ce deuxième article, vous devriez être capable d'installer un serveur Exim4 qui vous permettra de recevoir du courrier électronique, dans des conditions minimalistes.

Installation et configuration initiale:

Sous Debian, le travail d'empaquetage a été bien mené et un simple aptitude install exim4 devrait faire l'affaire. A retenir: Exim4 existe sous deux "formes": le démon "léger" (exim4-daemon-light) qui dispose uniquement de quelques modules et le démon "lourd" (exim4-daemon-heavy) qui vous permet par exemple de gérér l'authentification selon MySQL ou PgSQL, etc... Par défaut, c'est le démon "léger" qui est installé et, ça nous va bien pour débuter.

Pour configurer le MTA, un simple: dpkg-reconfigure exim4-config suffit.

Vous pouvez maintenant répondre aux questions qui vous sont posées par l'interface debconf:

  • Type de configuration : Choisir Distribution directe par SMTP (site Internet), c'est ce que nous voulons faire.
  • Nom de courriel du système : moi j'ai mis mon nom de domaine, à vous de mettre le vôtre (mondomaine.org).
  • Liste d'adresses IP où Exim sera en attente de connexions SMTP entrantes : 127.0.0.1 ; ::1; l'IP de votre serveur
  • Autres destinations dont le courriel doit être accepté : idem, on met le nom du domaine: (mondomaine.org)
  • Domaines à relayer : vide car nous ne relayons pas le courrier électronique
  • Machines à relayer : vide car idem !
  • Faut-il minimiser les requêtes DNS (connexions à la demande) ? : Non dans notre cas, sur une ligne ADSL ce n'est pas un problème.
  • Méthode de distribution du courrier local : Mailbox (pour commencer, c'est plus simple car mutt sait le lire sans aucune configuration).
  • Faut-il séparer la configuration dans plusieurs fichiers ?: Oui, c'est recommandé car le fichier de configuration monolithique est vraiment long à lire.

Ensuite, si vous êtes sur un réseau NATé (votre serveur de courrier électronique est situé derrière un routeur), il vous faudra effectuer une redirection de ports du port 25 de votre passerelle Internet vers le port 25 de votre serveur.

Après cette étape, et compte-tenu de l'ouverture de ce port 25, vous avez maintenant un vrai serveur de courrier électronique qui fonctionne. La configuration de base vous permet de recevoir des emails sur les comptes utilisateurs. Si vous avez un utilisateur toto, il peut donc recevoir du courrier sur l'adresse toto@mondomaine.org. Le courrier sera directement distribué dans son répertoire email au format mbox. S'il lance mutt sans configuration, il pourra lire ces messages. De plus, cet utilisateur pourra envoyer avec son MUA préféré (mutt ou la commande mail ou mailx ou directement avec la commande sendmail), uniquement en ayant un shell (ou une session graphique) depuis le serveur de courrier électronique, avec cette adresse email. Cette configuration par défaut est certes simple, mais elle est également efficace: le serveur n'est pas un relais ouvert (envoi uniquement depuis la machine du serveur) et l'envoi de messages électroniques sur les comptes systèmes est rerouté par défaut.

Ainsi, il dispose d'un service de courrier électronique qui respecte la chaîne historique telle que décrite dans l'article précédent. C'est un bon début et l'effort n'a pas été trop important: il a fallu répondre à une dizaine de question ! Les développeurs Debian ont donc bien travaillé pour nous et ce qui semblait être une usine à gaz (exim4 l'est vraiment) nous permet de disposer d'un service utilisable dès maintenant.

Principes de la configuration sous Debian:

En temps normal (c'est à dire si vous compilez Exim depuis les sources), le fichier de configuration d'Exim est différent de celui qui est présenté par Debian. Pour faire simple, Debian implémente un système de macros de configuration:

  • Les réponses aux questions dpkg-reconfigure sont dans un fichier de macros (/etc/exim4/update-exim4.conf.conf)
  • Des macros sont contenues dans des fichiers de macros dans la configuration d'exim4 (/etc/exim4/conf.d)
  • En plus de ces macros, des fichiers de modèle (template) sont situés dans le répertoire de configuration d'exim4 (/etc/exim4/conf.d)
  • Vous appelez la commande update-exim4.conf qui génère à partir des fichiers sus-cités le vrai fichier de configuration d'exim situé dans /var/lib/exim4/config.autogenerated.

Vous pouvez bien entendu modifier les fichiers de modèle directement dans /etc/exim4/conf.d mais, lors de la prochaine mise à jour du paquet dans Debian, on vous demandera si vous voulez installer la version du développeur ou conserver la vôtre. C'est pourquoi, Debian recommande l'utilisation de macros dans des fichiers séparés qui ne seront pas affectés par la mise à jour.

Nous essayerons, dans la mesure du possible, de gérer notre configuration par des macros. Nous allons donc créer le fichier /etc/exim4/conf.d/main/000_localmacros où nous renseignerons les valeurs des macros. Les exemples de configuration de cet article utiliseront cette mécanique.

Pour faire en sorte qu'Exim4 prenne votre nouvelle configuration en compte, il n'y a pas besoin d'appeler la commande update-exim4.conf. En effet, cette dernière ne fait que mettre à jour le fichier dans /var/lib/exim4. Pour que le serveur prenne en compte cette configuration, il faut aussi le redémarrer. Le script init.d d'exim4 inclus un appel à update-exim4.conf. Dans ces conditions, autant redémarrer le serveur avec ce script à chaque modification de configuration.

Gestion de l'enregistrement MX:

En dehors de la configuration d'Exim, vous devez indiquer à ceux qui voudrait vous écrire, le nom FQDN et l'adresse IP de votre serveur de courrier électronique au monde entier. Pour cela, on utilise le DNS en renseignant un enregistrement DNS particulier le champ Mail Exchange (MX). A vous de trouver comment faire pour ajouter cet enregistrement...

Gestion des alias:

Un truc tout bête mais essentiel. Vous disposez d'un compte système nommé toto. On peut lui écrire sur l'adresse toto@mondomaine.org. Néanmoins, pour des raisons de communication, un compte système est souvent peu explicite. Par exemple, l'utilisateur toto qui s'apelle en vérité Didier Lechat désire sans doute disposer d'une adresse didier.lechat@mondomaine.org.

Il suffit d'éditer le fichier /etc/aliases et d'ajouter la ligne:

didier.lechat: toto

Je vous conseille les actions suivantes:

  • Renseigner l'adresse du postmaster: le postmaster est le compte administrateur qui doit être prévenu en cas d'erreur ou de problème sur le serveur de courrier électronique.
  • Renseigner les alias pour les comptes systèmes si ce n'est déjà fait.
  • Ajouter les alias pour les comptes suivants:
    • abuse: pour qu'on puisse signaler les problèmes de réseau.
    • webmaster: l'adresse de contact de base pour votre site web.

Pour plus d'informations sur les alias de courrier électronique, vous pouvez lire la documentation de etc-aliases (man etc-aliases).

Gestion de la taille des messages:

La spécification SMTP SIZE permet au serveur d'indiquer la taille maximale totale d'un message reçu ou envoyé. Ainsi, le client qui veut envoyer un email trop gros pourra l'indiquer à l'utilisateur avant que celui-ci n'envoie tout le message au serveur et que ce dernier indique qu'il est trop gros.

La configuration par défaut d'Exim4 implémente l'extension SIZE. Un simple telnet localhost 25 suivi d'un EHLO test, donne le résultat suivant:

220 top.serveur.exim4 ESMTP Exim 4.72 Fri, 06 May 2011 10:11:14 +0200
EHLO World
250-top.serveur.exim4 Hello localhost [127.0.0.1]
250-SIZE 52428800
250-PIPELINING
250 HELP

La taille après 250-SIZE est exprimée en octets. Donc par défaut, 50Mo ce qui est pas mal. Si vous désirez modifier cette taille, vous devez modifier la macro MESSAGE_SIZE_LIMIT. Par exemple, pour limiter la taille totale du message à 30Mo (soit 31457280), il suffit d'ajouter la ligne suivante dans /etc/exim4/conf.d/main/000_localmacros :

MESSAGE_SIZE_LIMIT = 31457280

Au redémarrage d'Exim, la configuration est prise en compte et voici ce qu'il indique:

EHLO world
250-top.serveur.exim4 Hello localhost [127.0.0.1]
250-SIZE 31457280
250-PIPELINING
250 HELP

Conclusion

Avec ces quelques instructions, vous pouvez avoir un vrai MTA à peu près configuré correctement et qui vous permet de recevoir du courrier électronique. La prochaine fois, nous parlerons de chiffrement, d'authentification avant de rentrer plus en détails dans le coeur de configuration d'Exim4. Enfin, il nous reste à travailler sur le reste: mise en place d'un serveur IMAP, gestion basique du SPAM, pourquoi pas un webmail, etc...

Posted sam. 07 mai 2011 11:14:27 Tags:

Saturday Cat Blogging

Mimi pop

Copyright (c) Médéric RIBREUX

Well, it's saturday night cat blogging time for me too... Now, mimi-pop (my cat's name) you are worldwide known !

Posted sam. 07 mai 2011 20:38:30 Tags:

Solution de serveur de courrier électronique auto-hébergé (partie 3) : Une installation de Dovecot sous Debian

Introduction:

Nous avons un MTA à moitié configuré: il permet de recevoir des emails et d'en envoyer uniquement depuis le serveur. Pour consulter les emails il est, pour l'instant, obligatoire de le faire sur la machine locale. Si l'accès via SSH est permis, c'est très facile. Néanmoins, vous avez peut-être d'autres boîtes email à gérer et votre MUA (mutt/Thunderbird) est un outil dédié qui permet de concentrer tout ce courrier électronique en un seul endroit. donc, l'accès par SSH limite cette faculté de concentration... Il faut donc pouvoir lire ces courriels à distance, sans être connecté directement sur le serveur. Qui dit distance dit Internet, ce qui aboutit à nous faire penser à connexion chiffrée.

Ce que nous voulons est finalement assez simple: pouvoir accéder via un MUA (mutt/Thunderbird) à un compte IMAP sur une connexion chiffrée. L'authentification se fera avec un autre compte que le compte système (donc, un autre nom d'utilisateur et un mot de passe différent). L'intérêt de cette méthode est de ne pas compromettre le compte de l'utilisateur local en exposant, d'une manière ou d'une autre le couple login/password.

En terme de connexion chiffrée, nous allons opter pour l'utilisation unique du port 993 (protocole IMAP sur TLS). Il est possible d'utiliser le mécanisme STARTTLS sur le port standard (143) mais il arrive parfois que le client n'implémente pas cette méthode ou qu'une attaque en man-in-the-middle vienne modifier l'annonce STARTTLS et récupère le login/mot de passe en clair.

Faire du Maildir à la place de mbox:

Lors de mon dernier article sur le sujet, j'avais mis en place une "livraison locale" (la traduction le plus proche, à mon sens, de local delivery) dans le format mbox. Mais, pour des raisons pratiques, en IMAP, c'est mieux d'avoir ça sous forme de fichiers parce qu'on peut gérer une sorte d'arborescence. Nous allons donc modifier ça, non pas en rappelant dpkg-reconfigure exim4-config mais en lisant et trifouillant dans le fichier de configuration d'exim4. Cela nous permettra de nous familiariser un peu plus avec la complexe configuration d'exim.

Une petite recherche dans le fichier /var/lib/exim4/autogenerated.config nous apprend qu'il existe une macro nommée LOCAL_DELIVERY qui par défaut vaut mail_spool. Un petit tour du côté de mail_spool nous indique les élements suivants:

mail_spool:
  debug_print = "T: appendfile for $local_part@$domain"
  driver = appendfile
  file = /var/mail/$local_part
  delivery_date_add
  envelope_to_add
  return_path_add
  group = mail
  mode = 0660
  mode_fail_narrower = false

Il s'agit d'un transport car cette directive est présente après l'instruction begin transports du fichier de configuration. Sans rentrer trop dans les détails, on voit que ce transport utilise le pilote (driver) appendfile et on se doute bien que, vu son nom, il doit servir à ajouter du contenu à la fin d'un fichier. Ce fichier est nommé /var/mail/$local_part et on se doute également que $local_part doit correspondre à un truc du genre le login de l'utilisateur.

Si on continue l'exploration, on voit juste en dessous une autre directive nommée maildir_home et qui contient des paramètres qui semblent indiquer une sauvegarde en mode maildir dans le répertoire home. Ainsi, pour modifier le mode de livraison locale, il faut requalifier notre macro LOCAL_DELIVERY à maildir_home. Pour faire ça simplement, vous pouvez modifier le fichier /etc/exim4/update-exim4.conf.conf et indiquer maildir_home dans la directive dc_localdelivery.

Redémarrez exim4 et envoyez-vous des courriels, ça devrait marcher.

Pour configurer Mutt avec une boîte en Maildir, lisez le Wiki de Mutt. Pour notre part, nous allons aller plus loin et autoriser une consultation déportée du serveur.

Dovecot:

Ok, nous disposons de messages au format Maildir, celui-ci nous permet de mettre en place un service IMAP(S pour secure). Exim4 n'est qu'un MTA, il ne sait parler autre chose que le SMTP. Du coup, il nous faut un serveur IMAP qui fera le job de récupérer nos emails déposés dans \~/Maildir et les mettre à disposition de notre client IMAP (genre mutt).

Parmi les serveurs IMAP disponibles sur la distribution Debian, on peut distinguer Dovecot qui se dégage des autres par sa popularité et par le fait qu'il passe tous les tests de conformité IMAP. Notre objectif va donc être d'installer Dovecot sur le serveur et de faire en sorte qu'il utilise les emails stockés au format maildir.

Pour commencer, un simple aptitude install dovecot-imapd suffit à installer la partie serveur IMAP de dovecot. Attention, sous Debian Squeeze (l'objet de ce document), la version de Dovecot est 1.2. Dovecot est passé depuis à la version 2.0 qui apporte quelques modifications importantes. Ensuite, le reste se passe dans le fichier de configuration qui est bien monolithique (un seul fichier de 1200 lignes mais bien documenté): /etc/dovecot/dovecot.conf.

Dovecot est installé avec une documentation de qualité (présente dans le paquet dovecot-common): il s'agit d'un export du wiki de Dovecot. Dans notre cas, il est donc préférable de nous reporter aux éléments en ligne. Rendez-vous donc sur le wiki dédié à la version 1.x, celle qui est installée sous Debian Squeeze: http://wiki1.dovecot.org/ . Je vous recommande de lire une bonne partie de cette documentation. Elle vous permettra de vous familiariser avec le fichier de configuration de Dovecot.

TLS:

Sous Debian, un certificat et une clef privée sont automatiquement générées lors de l'installation de Dovecot. Cela permet de disposer d'un serveur sous TLS par défaut. Néanmoins, vous voulez peut-être générer ces fichiers vous même pour, par exemple, les signer avec votre propre CA.

Pour ma part, pour aller au plus simple, j'ai réutilisé le même certificat que pour la configuration TLS du serveur Web (après tout, on identifie mieux ce qu'on connaît déjà). Vous pouvez toutefois regénérer un certificat x509 autosigné avec la commande suivante (et en répondant correctement aux questions):

openssl req -new -x509 -nodes -out /etc/ssl/certs/dovecot.pem -keyout /etc/ssl/private/dovecot.pem
chmod 0600 /etc/ssl/private/dovecot.pem

Note: je ne maîtrise sans doute pas suffisamment TLS pour prétendre que cette configuration sera adaptée à votre utilisation.

Nous allons voir dans la suite, où modifier nos paramètres de configuration pour indiquer quels certificats utiliser.

Configuration:

Le fichier de configuration global est /etc/dovecot/dovecot.conf. Il est très complet. Par défaut, très peu de valeurs sont renseignées et toutes les variables de configuration sont présentes sous forme de commentaires. Dovecot n'a pas besoin de grand chose pour fonctionner car toutes les variables de configuration ont une valeur par défaut en dur dans le code...

Néanmoins, comparativement à nos besoins, nous pouvons apporter quelques modifications de notre cru:

# Nous ne voulons que du imaps
protocols = imaps

# Nous écoutons uniquement en IPv4 (pas bien) sur l'adresse de notre serveur
listen = 192.168.0.4
ssl_listen = 192.168.0.4

# Nous voulons supprimer toute authentification en clair:
disable_plaintext_auth = yes

# Nous ne voulons que du TLS:
ssl = required
# Voici les directives pour indiquer quels certificats utiliser
ssl_cert_file = /etc/ssl/certs/dovecot.pem
ssl_key_file = /etc/ssl/private/dovecot.pem

## Emplacement des boîtes email:
mail_location = maildir:~/Maildir

Je vous invite à lire le fichier de configuration par vous-même, c'est très instructif. Sinon, vous pouvez lire la référence sur le wiki. Pour la suite, je n'indiquerai que les éléments de configuration en rapport avec l'action ciblée.

Avant de poursuivre, il faut bien retenir que Dovecot est un logiciel serveur. Son but est de servir potentiellement plusieurs utilisateurs. Or, rien dans notre fichier de configuration ne fait référence à ces utilisateurs. C'est pourquoi Dovecot propose la gestion des particularités des utilisateurs selon le concept de base de données utilisateur. C'est dans ces bases qu'on peut configurer finement les éléments de configuration pour chaque compte. Les bases de données utilisateur peuvent être de vrai SGBD(R) comme MySQL mais aussi du LDAP et, bien entendu, des fichiers plats.

Gestion des quotas:

Cette gestion se fait par deux plugins spécifiques:

  • quota pour la gestion interne des limites.
  • imap_quota pour tout ce qui concerne la présentation des quota aux clients.

Pour activer ces deux plugins, il suffit de modifier la variable mail_plugins dans la partie protocol imap:

protocol imap {
 mail_plugins = quota imap_quota
}

Ensuite, il reste à configurer ces plugins. Tout se passe dans la partie plugin.

plugin {
  quota = maildir:Quota utilisateur
  quota_rule = *:storage=500M
  quota_rule2 = Trash:storage=100M
  quota_rule3 = *:messages=3000
  quota_exceeded_message = Bordel ! fais donc le ménage dans tes mails, yapu2place...
}

La variable quota indique la méthode de gestion du quota (ici maildir mais il en existe d'autres) ainsi que le message qui sera présenté au client. quota_rule est un nom de règle de quota qui indique la taille par défaut du quota (ici ,l'utilisateur a droit à 500M maxi). S'il utilise le répertoire Trash, il obtiendra 100Mo de plus. La règle n°3 indique que la boîte de l'utilisateur ne peut pas dépasser 3000 messages. Enfin, la variable quota_exceeded_message est self-explicit !

A noter que ces paramètres seront ceux par défaut qui seront appliqués aux utilisateurs. Une directive userdb peut modifier ces valeurs par défaut (voir la suite).

Configuration d'un utilisateur virtuel:

(inspirée de ça: http://wiki1.dovecot.org/HowTo/SimpleVirtualInstall ).

Voici le topo:

  • nous voulons nous authentifier sur le serveur Dovecot avec un login qui n'existe pas dans /etc/passwd, c'est à dire un utilisateur qui n'existe pas sur le système. Cet utilisateur aura pour login not_toto
  • Nous voulons que cet utilisateur virtuel dispose d'un mot de passe créé par nos soins. Comme c'est mal de stocker les passwords en clair, nous ne garderons ce mot de passe que sous forme d'un hash en utilisant la fonction CRAM-MD5 (d'autres fonctions sont implémentées). Nous choisissons cette implémentation car mutt la supporte très bien.
  • Nous voulons que cet utilisateur virtuel accède en lecture-écriture via Dovecot aux emails du compte système toto qui est servi directement par le MTA Exim4 dans le répertoire /home/toto/Maildir. Le compte système toto a pour uid et gid: 1000 et 1010
  • Nous voulons limiter la taille totale des messages à 300Mo.

Nous devons d'abord créer un fichier nommé /etc/dovecot/dovecot.passwd qui contiendra nos fausses références. Ce fichier contiendra un hash CRAM-MD5 de notre mot de passe. Voici comment le générer à l'aide de l'outil dédié (sous root): dovecotpw

dovecot -s cram-md5 -u no_toto
Enter new password: foo
Retype new password: foo
{CRAM-MD5}TOPHASHDNODS3ZrOq1bu2MasNk79LxHhlU9iI03

Il nous reste à inclure ce mot de passe et les élements cités plus haut dans notre fichier /etc/dovecot/dovecot.passwd:

not_toto:{SCRAM-MD5}TOPHASHDNODS3ZrOq1bu2MasNk79LxHhlU9iI03:1000:1010::/home/toto/::userdb_mail=maildir:/home/toto/Maildir userdb_quota_rule=*:storage=300M

Il reste à modifier la configuration de Dovecot pour prendre en compte ce fichier:

# dans la partie auth
auth default {
  mechanisms = cram-md5
  # notre méthode de vérification de mot de passe sera un fichier passwd
  passdb passwd-file {
    args = scheme=cram-md5 /etc/dovecot/dovecot.passwd
  }
  # notre méthode de récupération des infos utilisateur sera également un fichier passwd
  userdb passwd-file {
   args = /etc/dovecot/dovecot.passwd
  }
}

Vous pouvez maintenant tenter la consultation avec Mutt. Attention à votre configuration (.muttrc): par défaut, il semble que Mutt utilise l'authentification LOGIN et PLAIN. si vous êtes affectés par ce problème, vous ne pourrez pas vous connecter (connexion échouée). Vous devrez configurer la directive imap_authenticators de votre fichier .muttrc de telle manière que le mode d'authentification requis par votre configuration dovecot soit géré. Dans mon cas, l'ajout de set imap_authenticators="gssapi:cram-md5:login" règle le problème !

Amélioration des performances disques:

Comme indiqué ici, il faut veiller à ce que vous puissiez générer des index de répertoires dans votre sysfs. Sous Debian, c'est généralement le cas... mais on ne sait jamais !

Conclusion:

Voilà, avec ces élements, vous devriez être capable de monter un vrai service IMAP sur votre machine et consulter vos emails à distance. Dans notre projet de mise en place d'un service de courrier électronique complet, nous avons vu comment mettre en place la poste (Exim4), notre boîte aux lettres (fichiers locaux) et comment lire notre courrier sans être directement sur le serveur (Dovecot). Bien entendu, les fichiers dans Maildir restent disponibles pour d'autres programmes. Il nous reste à voir comment envoyer des emails depuis notre MTA, comment mettre en place du chiffrement au niveau de notre MTA.

Tout cela implique de se refocaliser sur Exim4 et fera l'objet d'un futur article...

Posted dim. 15 mai 2011 13:15:19 Tags:

Solution de serveur de courrier électronique auto-hébergé (partie 4): Lire des flux RSS/Atom avec feed2imap

Maintenant que nous disposons d'un vrai serveur IMAP et d'un serveur SMTP, nous pouvons utiliser le courrier électronique pour lire des flux RSS. Oui, utiliser le système du courrier électronique pour cela peut sembler farfelu... Mais après réflexion, pas tant que ça !

En effet, l'intérêt des flux Atom/RSS est de faire venir jusqu'à vous des informations que vous avez choisies de lire (parce que vous y êtes "abonné"). Néanmoins, naviguer dans ces flux peut être complexe et long. Si vous utilisez un navigateur Web, même si ce dernier propose des mécanismes de consultation, il ne sera pas facile de voir ce qui est nouveau, ce qui a déjà été lu. De même, vous ne pourrez pas conserver les articles qui vous intéressent, à moins de le faire à la main ce qui est fastidieux. C'est pour cela que des clients dédiés ont été développés, comme Liferea.

Ces derniers proposent de nombreuses fonctionnalités telles que:

  • le tri
  • la conservation de l'information
  • la recherche dans les flux lus
  • une gestion facilitée des abonnements,
  • etc...

Toutefois, un client lourd a un vrai problème: il n'est pas accessible à distance. En revanche, en utilisant un mécanisme qui bascule des flux RSS sous forme de courrier électronique, on dispose de toute la souplesse et l'accessibilité des protocoles d'envoi et de réception. Ainsi, avec un simple client email (qui peut être un webmail), vous pouvez lire, conserver, trier, renvoyer le contenu de ces flux quasiment de n'importe où sur Internet, le tout avec un seul outil. C'est un vrai avantage. Tout ce qu'il nous faut, c'est un logiciel capable de lire des flux RSS et de les injecter dans le système de courrier électronique.

Feed2imap est un programme développé en Ruby qui se charge de récupérer des flux RSS/ATOM et de les envoyer vers un dossier IMAP ou d'écrire directement dans un répertoire Maildir.

J'ai un peu hésité avant de l'installer. En effet, mon serveur n'a pas beaucoup d'espace disque et je dois mesurer chaque paquet qui est installé. De fait, n'ayant pas d'autres applications Ruby, l'installation de feed2imap va consommer plus d'espace que si j'avais utilisé un paquet du même genre: rss2email. Ce dernier est codé en Python mais il avait l'inconvénient majeur de ne supporter que l'envoi par SMTP des courriels, ce qui, sur une même machine est une perte de temps sachant que tout est stocké en local. Enfin, rss2email n'est pas franchement à jour dans l'archive Debian (même si certains ont déjà fait le boulot).

Revenons à feed2imap... Après inspection, l'installation de feed2imap et de toutes ses dépendances occupe moins de 10Mo. C'est raisonnable ! aptitude install feed2imap permet de l'installer en deux temps trois mouvements.

En terme de configuration, il faut éditer un fichier dans son répertoire home: \~/.feed2imaprc. Il existe une documentation bien détaillée de ce fichier de configuration dans /usr/share/doc/feed2imap/examples/feed2imaprc. Voici un exemple de ce fichier avec une cible en maildir:

feeds:
- name: Linuxfr
  url: https://linuxfr.org/news.atom
  target: maildir:///home/toto/Maildir/.Feeds.Linuxfr/
- name: Standblog
  url: http://standblog.org/blog/feed/rss2
  target: maildir:///home/toto/Maildir/.Feeds.Standblog/

Pour vérifier que cela fonctionne, vous devez lancer feed2imap dans un shell du serveur.

L'intérêt de notre maildir est de ne pas avoir à utiliser de serveur IMAP pour envoyer les flux. Tout se passe au niveau des fichiers locaux, rien ne part sur le réseau ou sur l'interface loopback. Du coup, on peut penser que c'est plus efficace. De plus, lorsque le répertoire n'existe pas, feed2imap se charge de le créer avant d'y injecter les flux. Inutile de passer du temps à les créer.

Enfin, s'il faut que je me connecte sur le serveur pour lancer feed2imap à chaque fois, ça ne va pas le faire. Il faut donc automatiser ce lancement. Deux fois par jour me semble une bonne fréquence, sauf le week-end. Pour ça, on va utiliser une crontab (utilisateur, ça suffit):

SHELL=/bin/bash
# Lancement de feed2imap
0 9,18 * * 1-5       /usr/bin/feed2imap >/dev/null 2>&1

Maintenant, pour consulter ces flux, vous n'avez plus qu'à utiliser votre MUA préférré et deux fois par jour, vous aurez des news fraîches directement dans vos dossiers IMAP. Mieux encore, si vous mettez en place un webmail, vous pourrez consulter ces flux depuis n'importe quel navigateur web... ce sera pour une prochaine fois !

Dans le prochain article nous parlerons de la configuration d'Exim4 pour permettre l'envoi SMTP depuis un client MUA situé ailleurs que sur le serveur.

Posted ven. 20 mai 2011 19:20:43 Tags:

From newsbeuter to maildir

Here is a small Python script which can help you migrate the RSS items you have kept under newsbeuter to a maildir-based system. It doesn't pretend to be an extra-quality code and I had less than 2 hours to discover SQLite3 and mailbox python modules to build the script. Some elements are hardcoded: you'll have to manually indicate to which rss feed URL (stored in newsbeuter) corresponds the maildir directory.

Here is the code:

  
  #!/usr/bin/python
  # -*- coding: utf-8 -*-

  # Put your newsbeuter cache messages in Maildir directories

  # This program is free software: you can redistribute it and/or modify
  # it under the terms of the GNU General Public License as published by
  # the Free Software Foundation, either version 3 of the License, or
  # (at your option) any later version.

  # This program is distributed in the hope that it will be useful,
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  # GNU General Public License for more details.

  # You should have received a copy of the GNU General Public License
  # along with this program.  If not, see <http://www.gnu.org/licenses/>.

  # Newsbeuter stores its feeds in ~/.newsbeuter/cache.db. This file is a
  # SQLite3 database with 2 tables. The table named rss_item contains all
  # of the RSS/Atom messages. The "body" of the message is stored in HTML
  # (without the headers).

  # This Python script converts the messages of your cache.db file to
  # Maildir repositories. The RSS/Atom messages are put in their
  # respective Maildir...

  # modules import
  from email.mime.text import MIMEText
  import email.utils
  import mailbox
  import sqlite3
  import sys

  # Dictionary for feedurl to Maildir directories association:
  # You have to manually change thoses references for your feeds
  # Keys are feedurl from newsbeuter, items are the name of the sub-maildir
  feed2mail = { 'http://www.maitre-eolas.fr/feed/atom':'Feeds.Eolas',
                'https://medspx.fr/blog/index.rss20':'Feeds.WhereIsIt',
                'http://planet.debian.net/rss20.xml':'Feeds.planetDebian',
                'http://feeds2.feedburner.com/hackaday/LgoM':'Feeds.HackADay',
                'https://www.adafruit.com/blog/feed/':'Feeds.Adafruit',
                'https://linuxfr.org/news.atom':'Feeds.LinuxFr',
                'http://www.bortzmeyer.org/feed-full.atom':'Feeds.Bortzmeyer',
                'http://www.la-grange.net/feed.atom':'Feeds.LaGrange'}

  # Part 1: Arguments analysis
  if len(sys.argv) < 2:
      print 'newsbeuter2maildir.py cache.db_file maildir'
      exit(1)

  cache_file = sys.argv[1]
  maildir = sys.argv[2]

  print 'extracting from %s to %s ...' % (cache_file, maildir)

  # Part 2: extract messages from cache.db
  cache = sqlite3.connect(cache_file)
  destination = mailbox.Maildir(maildir)

  c = cache.cursor()
  # We only want to extract the items that have been read and which are
  # not deleted
  c.execute('select title, author, feedurl, pubDate, content from'
           + ' rss_item where unread=0 and deleted=0')
  # for every line, we put a message in the right mailbox
  for line in c:
      header = u'<html><head></head><body>'.encode('utf-8')
      footer = u'</body></html>'.encode('utf-8')
      msg = MIMEText(header+line[4].encode('utf-8')+footer, 'html')
      msg['Subject'] = line[0].encode('utf-8')
      msg['From'] = line[1].encode('utf-8')
      msg['Date'] = email.utils.formatdate(float(line[3]))
      if line[2] in feed2mail.keys():
          feed = feed2mail[line[2]]
          # If the maildir doesn't exist, we create it.
          if feed not in destination.list_folders():
              print "maildir", feed, "doesn't exist: we create it !"
              maildir_feed = destination.add_folder(feed)
          else:
                maildir_feed = destination.get_folder(feed)
          # Add the message to the maildir and mark it unread:
          msg_in_feed = mailbox.MaildirMessage(msg)
          msg_in_feed.set_flags('S')
          msg_key = maildir_feed.add(msg_in_feed)
      else: 
          print "Unknown Feed:",line[2]

  destination.close()

  exit(0)
Posted mer. 25 mai 2011 14:05:44 Tags:

Ca fait un an que je suis parti...

Il y a un an, jour pour jour, j'entamais ce qu'on peut qualifier de périple en solitaire dans les Pyrénées. Avec seulement un sac sur le dos, j'ai marché dans ces montagnes magnifiques pendant près de 15 jours. Ce fut une expérience mémorable... J'en rêve encore la nuit et les souvenirs sont encore bien marqués. Bien souvent, alors que je suis endormi, je reconstruit mentalement mon parcours.

J'ai couché sur le papier électronique un récit de cette "aventure". Je l'avais publié sur ce même site web au format PDF. Pour fêter cet anniversaire, je publie ces récits dans des formats plus adaptés pour les lecteurs de livres électroniques: MOBI et ePub. Ne disposant que d'un Kindle3, je n'ai testé que la version MOBI en réel. Pour le reste, je fais confiance à Calibre qui est le logiciel que j'ai utilisé pour la conversion depuis le format OpenDocument Text.

Ces fichiers étant publiés sous Licence Creative Commons Attribution ShareAlike 3.0 unported (rien que ça !).

Posted dim. 29 mai 2011 13:06:35 Tags: