Have you ever been curious as to which country your e-mail messages are sent from? Simply looking at the domain doesn’t always help, as the ubiquitous .com domains for example don’t necessarily originate from the US.

I am interested so I have implemented a Geo-IP lookup on the sender’s host address. In order to have that visualized in e-mail clients, I’ll have the mail server add a custom header containing the two-letter ISO 3166 country code to incoming messages. How is a country determined from a simple IP address? There are providers of databases which can map an IP address to a country with varying degrees of precision, depending on what I’m willing to pay for the service. One of these is MaxMind: they offer subscription services to their premium data, but they also have a database named GeoLite Country which is free to use. Together with their snazzy little C API, these two offerings form the basis with which to implement the lookup. Download and install the C API to get the database and the library on your system. As soon as the library and its corresponding database are installed, make a point of retrieving the latest version of the database. Instructions on how to do this are contained in the file data/README. That is it. The system can be tested with the geoiplookup program.

$ geoiplookup
GeoIP Country Edition: KR, Korea, Republic of

One way to determine the countries of origin of messages is to scan through mail logs to collect a list of IP addresses with which to feed geoiplookup. Boring! I want it done on the fly. :-) Now to the MTA. Since version 4.51, Exim supports calling external C functions from loadable modules. These are loaded dynamically by Exim and the result of the custom function can be used within a string expansion. Follow the instructions for enabling dlfunc in Local/Makefile to the letter, or else the loadable modules won’t work. If your version of Exim has support for dlfunc, the loadable module is instantiated and invoked with a call to a string expansion of the form


, where file.so is the full path to your shared object, f the name of the C function to be called, and a0 an optional argument of which there can be a maximum of eight. David Saez created an a few of these custom functions, including one aptly named ip2country which utilizes MaxMind’s C API to query the Country database. A small warning: don’t blindly trust the Makefile’s install target: you’ll probably want to uncomment the second line. I’ve inserted a call to the function in the smtp_data ACL somewhere, like so. I have a condition which ensures the function is only called on messages originating on the Internet and not from internal MXen, but I leave that as an excercise to the reader:

warn condition = ${if ...  }{0}{1}}
     add_header = X-senderGeoIP: ${dlfunc{/usr/exim/bin/exim-ext.so}{ip2country}{$sender_host_address}}

This causes the specified header to be added to incoming messages (in some older Exim versions, use message instead of add_header). In MUA that support it, I can see the header nicely.

X-senderGeoIP: DE

Because I access most of my mail with Mutt, I want to view the country code in the index. Mutt has support for the non-standard


header, which can be made visible with the


switch in the $hdr_format variable. Procmail filters my messages, and I’ll coerce it to add an


header with the value of


. First a wee bit o’

# Grab the country (Geo-IP)
XCOUNTRY=`formail -cx "X-senderGeoIP"`

# If the message has an X-senderGeoIP header, set the X-Label
# header on it, so that Mutt can display it in its indices.

:0 fhw
* ^X-senderGeoIP:.*
| formail -i "X-Label: $XCOUNTRY"

You’ll be asking yourself why I didn’t directly use the X-Label header when determining the country code in the MTA. Good question. I don’t want to clobber a header that other MUAs might be using, preferring to clobber it only on my own mail. I like the result: here is a partial index of my current spam shown in Mutt with a hdr_format setting of

"%4C %Z %%b %d %-15.15F (%4c) [%?Y?%-3.3y&---?] %s"

: Mutt Now to Lotus Notes. The Inbox folder’s design needs to be updated to contain the content of the header we inserted. Unfortunately, that appears to be easier said than done, as I failed miserably doing that. I solved it in a rather convoluted way by having a Lotusscript agent set the field before new mail arrives.

Sub Initialize
	' XsenderGeoIP. Agent, before new mail arrives. Had to do this
	' because I can't get at the original field with @Formula.
	Dim s As New NotesSession
	Dim db As NotesDatabase
	Dim doc As NotesDocument
	Dim mime As NotesMIMEEntity
	Set db = s.CurrentDatabase
	Dim geo As String
	s.ConvertMIME = False ' Do not convert MIME to rich text
	Set doc = s.DocumentContext
	With doc
		Set mime = doc.GetMIMEEntity
		If Not(mime Is Nothing) Then
			Dim filter(1) As String
			filter(0) = "X-senderGeoIP"
			geo = mime.GetSomeHeaders(filter, True)
			geo = Right(geo, 2)
			doc.xxgeo = geo
			Call doc.save(True, False)
		End If
	End With
	s.ConvertMIME = True ' Restore MIME conversion
End Sub

The final result is then: Lotus Notes E-mail with added value for three of us: me, myself and I. :-)

Mail, DomiNotes, Exim, and Database :: 28 Aug 2007 :: e-mail