Taavi V n nen: Tracking my train travel by parsing tickets in emails
Rumour has it that I might be a bit of a train nerd. At least I want to
collect various nerdy data about my travels. Historically that data has
lived in manual form in several places,1 but over the past year and
a half I've been working on a toy project to collect most of that
information into a custom tool.
That toy project2 uses various sources to get information about
trains to fill up its database: for example, in Finland Fintraffic, the
organization responsible for railway traffic management, publishes very
comprehensive open data about almost everything that's moving on the
Finnish railway network. Unfortunately, I cannot be on all of the
trains.3 Thus I need to tell the system details about my journeys.
The obvious solution is to make a form that lets me save that data.
Which I did, but I got very quickly bored of filling out that form, and
as regular readers of this blog know, there is no reason to settle for
a simple but boring solution when the alternative is to make something
that is ridiculously overengineered.
Parsing data out of my train tickets
Finnish long-distance trains generally require train-specific seat
reservations, which means VR (the train company) knows which trains
I am on. We just need to find a way to extract that information in some
machine-readable format. So my plan for the ridiculously overengineered
solution was to parse the booking emails to get the details I need.
Now, VR ticket emails include the data I want in a couple of different
formats: they're included as text in the HTML email body, they're in
the embedded calendar invite, as text in the included PDF ticket, and
encoded in the Aztec Code in the included PDF ticket. I chose to
parse the last option with the hopes of building something that could
be ported to parse other operators' tickets with relative ease.
Parsing data out of my train tickets
Finnish long-distance trains generally require train-specific seat
reservations, which means VR (the train company) knows which trains
I am on. We just need to find a way to extract that information in some
machine-readable format. So my plan for the ridiculously overengineered
solution was to parse the booking emails to get the details I need.
Now, VR ticket emails include the data I want in a couple of different
formats: they're included as text in the HTML email body, they're in
the embedded calendar invite, as text in the included PDF ticket, and
encoded in the Aztec Code in the included PDF ticket. I chose to
parse the last option with the hopes of building something that could
be ported to parse other operators' tickets with relative ease.
Example Aztec code
After a bit of digging (thank you to the KDE Itinerary people for
documenting this!) I stumbled upon an European Union Agency for
Railways PDF titled ELECTRONIC SEAT/BERTH RESERVATION AND ELECTRONIC
PRODUCTION OF TRANSPORT DOCUMENTS - TRANSPORT DOCUMENTS (RCT2
STANDARD) which, in its Appendix C.1, describes how the information is
encoded in the code.4 (As a side note, various sources call these
codes SSB version 1 codes, although that term isn't used in this
specification. So maybe there are more specifications about the format
that I haven't discovered yet!)
I then wrote a parser in Go for the binary data embedded in these
codes. So far it works, although I wouldn't be surprised if there are
some edge cases that it doesn't handle. In particular, the spec
specifies a custom lookup table for converting between text and binary
data, and that only has support for characters 0-9 and A-Z. But Finnish
railway station codes can also use and .. maybe I need to buy a
ticket to a station with one of those.
Extracting barcodes out of emails
A parser just for the binary format isn't enough here if the intended
source input is the emails that VR sends upon making a booking. Time to
write a single-purpose email server! In short, the logic in the server,
again written in Go and with the help of go-smtp and go-message,
is:
- Accept any mail with a reasonable body size
- Process through all body parts
- For all PDF parts, extract all images
- For all images, run them through ZXing
- For all decoded barcodes, try to parse them with my new ticket
parsing library I mentioned earlier
- If any tickets are found, send the data from them and any metadata
to the main backend, which will save them to a database
The custom mail server exposes an LMTP interface over TCP for my
internet-facing mail servers to forward to. I chose LMTP for this
because it seemed like a better fit in theory than normal (E)SMTP.
I've since discovered that curl doesn't support LMTP which makes
development much harder, and in practice there's no benefit of LMTP
here as all mails are being sent to the backend in a single request
regardless of the number of recipients, so maybe I'll migrate it to
regular SMTP at some point.
Side quest time
The last missing part is automatically forwarding the ticket mails to
the new service. I've routed a dedicated subdomain to the new service,
and the backend is configured to allocate addresses like
i2v44g2pygkcth64stjgyuqz@somedomain.example for each user. That's
great if we wanted to manually forward mails to the service, but we can
go one step above that. I created a dedicated email alias in my mail
server config that routes both to my regular mailbox and the service
address. That way I can update my VR account to use the alias and have
mails automatically processed while still receiving backup copies of
the tickets (and any other important mail that VR might send me).
Unfortunately that last part turns out something that's easier said
than done. Logging in on the website, I'm greeted by this text stating
I need to contact customer service by phone to change the address
associated with my account.5 After a bit of digging, I noticed that
the mobile app suggests filling out a feedback form in order to change
the address. So I filled that, and after a day or two I got a "confirm
you want to change your email" mail. Success!
-
Including (but not limited to): a page of this website, the notes
app on my phone, and an uMap map.
-
Which I'm not directly naming here because I still think it needs
a lot more work before being presentable, but if you're really
interested it's not that hard to find out.
-
Someone should invent human cloning so that we can fix this.
-
People who know much more about railway ticketing than I do were
surprised when I told them this format is still in use somewhere.
So, uh, sorry if you were expecting a nice universal worldwide
standard!
-
In case you have not guessed yet, I do not like making phone
calls.
- Accept any mail with a reasonable body size
- Process through all body parts
- For all PDF parts, extract all images
- For all images, run them through ZXing
- For all decoded barcodes, try to parse them with my new ticket parsing library I mentioned earlier
- If any tickets are found, send the data from them and any metadata to the main backend, which will save them to a database
curl doesn't support LMTP which makes
development much harder, and in practice there's no benefit of LMTP
here as all mails are being sent to the backend in a single request
regardless of the number of recipients, so maybe I'll migrate it to
regular SMTP at some point.
Side quest time
The last missing part is automatically forwarding the ticket mails to
the new service. I've routed a dedicated subdomain to the new service,
and the backend is configured to allocate addresses like
i2v44g2pygkcth64stjgyuqz@somedomain.example for each user. That's
great if we wanted to manually forward mails to the service, but we can
go one step above that. I created a dedicated email alias in my mail
server config that routes both to my regular mailbox and the service
address. That way I can update my VR account to use the alias and have
mails automatically processed while still receiving backup copies of
the tickets (and any other important mail that VR might send me).
Unfortunately that last part turns out something that's easier said
than done. Logging in on the website, I'm greeted by this text stating
I need to contact customer service by phone to change the address
associated with my account.5 After a bit of digging, I noticed that
the mobile app suggests filling out a feedback form in order to change
the address. So I filled that, and after a day or two I got a "confirm
you want to change your email" mail. Success!
-
Including (but not limited to): a page of this website, the notes
app on my phone, and an uMap map.
-
Which I'm not directly naming here because I still think it needs
a lot more work before being presentable, but if you're really
interested it's not that hard to find out.
-
Someone should invent human cloning so that we can fix this.
-
People who know much more about railway ticketing than I do were
surprised when I told them this format is still in use somewhere.
So, uh, sorry if you were expecting a nice universal worldwide
standard!
-
In case you have not guessed yet, I do not like making phone
calls.
- Including (but not limited to): a page of this website, the notes app on my phone, and an uMap map.
- Which I'm not directly naming here because I still think it needs a lot more work before being presentable, but if you're really interested it's not that hard to find out.
- Someone should invent human cloning so that we can fix this.
- People who know much more about railway ticketing than I do were surprised when I told them this format is still in use somewhere. So, uh, sorry if you were expecting a nice universal worldwide standard!
- In case you have not guessed yet, I do not like making phone calls.
With a friendly Canadian hand wave from vacation in Beautiful British
Columbia, and speaking on behalf of the Rcpp Core Team, I am excited to
shared that the (regularly scheduled bi-annual) update to
Another short status update of what happened on my side last
month.
My Debian contributions this month were all
As I often do, this year I have also prepared a set of personalized maps
for your OpenPGP keysigning in DebConf25, in Brest!
What is that, dare you ask?


Single signon is a pretty vital part of modern enterprise security. You have users who need access to a bewildering array of services, and you want to be able to avoid the fallout of one of those services being compromised and your users having to change their passwords everywhere (because they're clearly going to be using the same password everywhere), or you want to be able to enforce some reasonable MFA policy without needing to configure it in 300 different places, or you want to be able to disable all user access in one place when someone leaves the company, or, well, all of the above. There's any number of providers for this, ranging from it being integrated with a more general app service platform (eg, Microsoft or Google) or a third party vendor (Okta, Ping, any number of bizarre companies). And, in general, they'll offer a straightforward mechanism to either issue OIDC tokens or manage SAML login flows, requiring users present whatever set of authentication mechanisms you've configured.





