Envoyer des fax depuis Rails via OVH

Dans toute ma vie, j’ai du envoyer ou recevoir moins de fax que j’ai de doigts à toutes mes mains (et contrairement à certains, ça ne me fait que 10 doigts).
En plus, en bon artisan du web, ce genre de technologie d’avant me fait plutôt rire qu’autre chose.

Mais force est de constater que certains métiers sont encore très attachés à ce genre de communication. Pour RoomRoom (un des sites sur lesquels je travaille ces derniers temps), nous devons communiquer des informations importantes à des hôteliers, pour qui l’e-mail n’est pas vraiment le moyen le plus fiable. Le bon vieux fax qui arrive directement derrière le comptoir de la réception, ça par contre, ça fonctionne bien et ça laisse une trace.

Donc, comment envoyer un fax depuis une application Rails ?

Le transport

Je n’envisage pas de brancher un modem sur mes serveurs, mais les services « en ligne » d’envoi de fax sont légions. Il est possible que certains aient même une API, mais le plus souvent il faut leur transmettre un document (idéalement un PDF), c’est pourquoi ça passe souvent par l’e-mail.

  • envoyer un e-mail : on sait faire ;
  • avec une pièce jointe : c’est très facile aussi ;
  • une pièce jointe en PDF, générée dynamiquement : c’est un peu plus sport mais on va y arriver.

Pour mon essai, j’ai choisi la solution EcoFax d’OVH. C’est du basique, mais ça marche et ça coûte 3 kopeks pour une utilisation occasionnelle (quelques envois par jour).

Alors concrètement, puisque notre appli Rails va envoyer un mail, on va pouvoir s’appuyer sur ActionMailer.

Le contenu de cet e-mail se résume à ceci :

  • le champ From déterminera l’expéditeur du fax ;
  • le champ To, au format 0123456789@ecofax.fr déterminera le destinataire du fax (ici 0123456789) ;
  • le champ Subject doit être notre identifiant (ici 0987654321) ;
  • le corps du message contient d’autres paramètres : au minimum password: AZERTYIUOP, mais en option la qualité, l’heure d’envoi…

Pour plus de détail, voici la FAQ d’OVH sur le sujet.

Voici donc un exemple de mailer :

class FaxMailer < ApplicationMailer
  def hotel_offer_on_sale(offer)
    file_name = "roomroom_offer#{offer.id}_on_sale.pdf"
    file = prepare_on_sale_fax_file(offer)
    attachments[file_name] = file.read

    config = Rails.application.secrets.ecofax

    mail(
      to: config.fetch("to") % { number: offer.hotel.fax_number },
      subject: config.fetch("login"),
      body: "password: #{config.fetch("password")}"
    )
  end
end

Les 3 premières lignes permettent de générer un fichier PDF attaché (détail plus loin), puis on envoie simplement le mail.

Voici l'extrait du fichier config/secrets.yml qui contient les éléments référencés :

production:
ecofax:
to: "%{number}@ecofax.fr"
login: "0987654321"
password:

Si on fait abstraction de la méthode prepare_on_sale_fax_file() on a quelque chose de fonctionnel, qui est capable d’envoyer un PDF attaché dans un e-mail.

Le contenu du fax

On pourrait utiliser une bibliothèque pour générer du PDF. Il existe en particulier Prawn. Mais c’est assez lourd et si on a déjà créé des vues pour notre application, on voudrait bien les réutiliser.

Dans notre cas, le fax est la copie presque parfaite d’une « mise en page HTML » également envoyée par e-mail. Du coup, on a tout ce qu’il nous faut.

Allons-y étape par étape. Comment générer un fichier HTML spécialement pour le fax ?

Le message en HTML

Dans Rails, les templates sont dans app/views/XXX_mailer/YYY, où XXX est le nom du mailer, et YYY est le nom de la méthode utilisée pour l’envoi du mail.

Puisque nous avons déjà un template, nous allons devoir l’adapter. En effet il n’y a pas de lien cliquable dans un fax, de même la feuille de style peut être différente pour optimiser la lisibilité en noir et blanc.

On va utiliser le principe de variante de Rails. En gros, ça permet de définir des variations sur un format, pour un périphérique particulier. Ici, on va utiliser le format « html » et définir une variante « fax ».

Notre template sera alors dans app/views/fax_mailer/hotel_offer_on_sale.html+fax.erb.

Voici un premier jet pour notre méthode de génération du fichier joint

def prepare_on_sale_fax_file(offer)
  @body = OpenStruct.new(
    offer_title: offer.title,
    offer_url: offer_url(slug: offer.slug),
    root_url:  root_url,
  )
  html = render_to_string template: "notification_mailer/hotel_offer_on_sale",
                          format: "html", variant: "fax"
end

Notez que j’ai choisi de ne créer qu’une seule variable d’instance (qui sera visible dans mon template). Une ivar pour chaque valeur marche très bien aussi, mais je trouve ça moins pratique et prévisible.

De plus, je génère les URL depuis le mailer et pas depuis la vue, ça économise des soucis plus tard, et ça simplifie la vue qui se contente d’injecter les valeurs dans le template.

On utilise ensuite la méthode render_to_string qui va renvoyer une chaîne avec le résultat du template généré. Le format « html » et la variante « fax » sont explicitement indiqués pour forcer le rendu en utilisant ces réglages là.

Là ça ne marche pas encore, car la méthode renvoie une longue chaîne de caractères alors que le mailer attends un IO (ou en tous cas un objet qui réponde à read()).

Convertir le HTML en PDF

Il existe une très bonne bibliothèque pour transformer du HTML en PDF (ou en image) : wkhtmltopdf.

Elle est disponible sous forme de paquet pour de nombreuses distributions. Ses sources sont également disponibles.

La version 0.9 disponible (notamment) dans le dépôt officiel Debian nécessite un serveur X (pas cool sur un serveur), mais une version 0.12 du paquet est dispo pour une installation manuelle.

Voilà comment je l’ai installée (sous Debian) :

aptitude install xfonts-base xfonts-75dpi
cd /tmp
wget http://downloads.sourceforge.net/wkhtmltopdf/wkhtmltox-0.12.2.1_linux-wheezy-amd64.deb
dpkg -i wkhtmltox-0.12.2.1_linux-wheezy-amd64.deb

Nous n’allons pas l’utiliser en direct, mais plutôt via une gem Ruby : WickedPdf. Elle se chargera de faire un appel à wkhtmltopdf avec les bon paramètres.

Voici l’évolution de notre méthode :

def prepare_on_sale_fax_file(offer)

  @body = OpenStruct.new(
    offer_title: offer.title,
    offer_url: offer_url(slug: offer.slug),
    root_url:  root_url,
  )
  html = render_to_string template: "fax_mailer/hotel_offer_on_sale",
                          format: "html", variant: "fax"
  pdf = WickedPdf.new.pdf_from_string(html)

  # On utilise un StringIO plutôt qu'un fichier sauvegardé
  # mais l'interface est la même `#read()` pour accéder au contenu
  StringIO.new(pdf)
end

Là nous avons maintenant une chaîne de caractère qui contient le code PDF, que nous embarquons au final dans un StringIO afin qu’elle se comporte comme un descripteur de fichier.

Et voilà !

Optimiser du rendu

Le fax n’est pas une technologie qui apporte une grande qualité, surtout à l’arrivée sur des vieilles machines.

Pour une meilleure lisibilité, je vous conseille une page sur fond blanc, avec du texte suffisamment gros, écrit en noir.

Si vous avez un logo, utilisez la version monochrome que votre graphiste aura spécialement préparée (si vous travaillez avec STPO, il y pense à chaque fois).

Evitez les ombres portées (de texte ou de bloc), éviter les traits très fins, les dégradés de couleur…

Pour avoir un bon rendu au niveau du PDF, utilisez une feuille de style et une mise en page simple. Dans mon cas, c’était déjà très simple au départ car le template était optimisé pour un rendu dans un client mail, donc je n’ai presque rien modifié.

Publié dans Informatique | Laisser un commentaire

RoomRoom (ex. Troc de Chambre) ; le dernier né

Je vais faire comme chez moi et je ne vais pas me gêner pour vous parler un peu du dernier projet sorti de chez Autrement, la boîte où je bosse depuis plusieurs années :

RoomRoom, où comment revendre sa chambre d’hôtel non-annulable et trouver des bons plans à prix réduits.

Revendez votre réservation d'hôtel

En plein marasme d’automne, on a eu l’idée, on l’a travaillée, mise à l’épreuve de notre milieu et on a décidé de se lancer dans le développement d’une version bêta pour la tester en vrai.

Un grand nombre de réservations d’hôtels ont des conditions qui les rendent non-annulables et/ou non-remboursables. Changement de planning, incident, maladie … sont autant de raisons qui empêchent trop souvent de profiter de ces réservations. Tout ou partie du prix payé est perdu. Pourquoi ne pas revendre cette réservation (lorsque les conditions le permettent), moins cher pour les rendre attractives, et récupérer une partie de la perte ?

roomroom_offres

Un exemple d’offres en ligne

 

C’est ce que nous avons voulu vérifier par nous mêmes. Et comme on a un peu d’expérience sur le sujet des hôtels en ligne et du dev web, on a réussi à lancer une bêta en peu de temps.

Voilà quelques éléments plus ou moins techniques pour les curieux :

DigitalOcean

On a voulu tester une plateforme très bon marché, quasi prête à l’emploi et ressemblant plus ou moins à du cloud.

C’est sûr que 10$/mois pour une machine permettant de faire tourner un petit site, tout en ayant la main complètement sur le système sous-jacent, c’est alléchant. Malheureusement, le bilan n’est pas si positif que ça. C’est probablement notre manque d’expérience dans l’automatisation … qui n’a pas aidé.

On va revenir à nos amours habituelles, chez Evolix, qui gère hyper bien nos serveurs dédiés depuis le début.

Ruby on Rails 4.2

On est assez rôdé sur Rails, le choix était donc assez simple, mais c’est bien de temps en temps de lancer un projet vierge sur la dernière version d’un framework, sans le poids de l’existant, et de pouvoir tester les bonnes pratiques du moment.

PostgreSQL 9.3

Utilisateurs principalement de MySQL et un petit peu de SQLite, on savait depuis longtemps que PostgreSQL avait des choses intéressantes à proposer. On a donc sauté le pas, un peu lutté au début, mais on est bien content du résultat. Ça vaut le coup. Surtout que Rails 4.x exploite vraiment de plus en plus les spécificités de PostgreSQL.

Mailjet

Pour le moment, c’est Mailjet qui gère la partie « envoi des mails ». je ne suis pas un fan absolu, mais ça fait le boulot. Et puis c’est une boîte française, donc j’apprécie de les faire bosser.

MangoPay

Un point important de RoomRoom ; nous sommes tiers de confiance entre l’achat sur le site et le paiement du vendeur une fois le séjour passé.

Il était évidemment hors de question de tout gérer nous-mêmes, pour des raisons techniques et légales. On a choisi de travailler avec MangoPay qui propose une API bien fichue qui permet de gérer des paiements entrants (par CB), des paiements sortants (virements SEPA) et tous les transferts internes qu’on souhaite au niveau de portefeuilles électroniques.

Le coût n’est pas négligeable (1,8% + 18cts, pour chaque paiement entrant) mais la richesse et la facilité de prise en main de l’API en valent le coup, du moins tant qu’on ne gère pas des millions d’euros.

Dans un contexte légal européen, les contraintes sont assez fortes, mais apportent une vraie confiance dans le système. Le dossier d’ouverture de compte est épais et riche, mais ça aussi, ça vaut le coup.

SSL/TLS et certificats X.509

Pour un site de e-commerce qui manipule des données personnelles, il faut bien proposer un sécurisation des échanges. L’acquisition de compétence sur le sujet m’a poussé à l’écriture d’un guide complet sur la mise en place d’un certificat SSL/TLS.

Au final, toute la navigation est en https, avec le maximum de ce qu’on sait faire à ce jour en terme de sécurité.

Un thème pas cher

Pas moyen de se lancer dans une grosse phase de webdesign, identité visuelle, etc. sans savoir si l’idée va plaire. On a acheté un thème pas cher, qu’on a légèrement adapté. C’est bien sûr plein de défauts, mais ça donne un résultat assez propre, compatible, responsif … en peu de temps.

Il sera toujours temps de changer plus tard si la phase de beta-test est concluante.

La suite du projet

Actuellement on est clairement en version bêta. On a un produit qui marche pour de vrai ; vous pouvez vendre, vous pouvez acheter et tout se passera bien. Mais les fonctionnalités sont limitées et on va devoir faire beaucoup de choses à la main.

On va continuer à ajouter des fonctionnalités, polir l’interface utilisateur, enrichir notre compréhension des singularités de la réservation hôtelière en ligne, tisser des relations avec les acteurs de l’industrie pour favoriser l’adhésion à l’idée.

On va aussi beaucoup écouter nos premiers clients et utilisateurs, communiquer sur les réseaux pour faire connaître l’idée.

Toute l’équipe d’Autrement est remontée à bloc, et ça fait du bien.

Mise à jour, 4 mai 2015 : j’ai modifié l’article et la capture d’écran, vu que Troc de Chambre a été rebaptisé RoomRoom.

roomroom_presse

Publié dans Autrement, Informatique, Uncategorized | Tagué , | 4 commentaires

Gandi SSL avec Nginx

Je me suis récemment penché sur un sujet complètement nouveau pour moi : les certificats SSL (pour chiffrer la consultation d’un site web).

J’étais frustré du peu d’information « à mon niveau » que je trouvais sur le web. J’ai décidé de prendre les choses en main et écrire le guide que j’aurais aimé trouver.

Le guide est ici : http://jlecour.github.io/ssl-gandi-nginx-debian/

Toute le code source de ce guide et les fichiers associés sont disponibles sur GitHub : https://github.com/jlecour/ssl-gandi-nginx-debian

Publié dans Informatique | Tagué , , , | 1 commentaire

Empathie

Je viens de lire ce tweet de Sam Sheppard :

Saying « Why are you depressed? Your life is great. » is like saying « What do you mean you have asthma? There is plenty of air in here. »

En français :

Dire « Pourquoi es-tu déprimé, la vie est belle. » c’est comme dire « Comment ça tu as de l’asthme ? Il y a plein d’air partout. »

Pour moi ça révèle un manque d’empathie, une incapacité à se mettre dans le peau de l’autre.

J’adhère parfaitement à ce propos, et pourtant j’ai déjà été coupable de ce genre de remarque. J’ai déjà dit des choses similaires à un de mes enfants lorsqu’il éclatait de colère et disait que je ne pouvais pas comprendre à quel point sa vie est difficile et que ses problèmes sont durs à gérer.

C’était un moment de colère partagé, où ma capacité à écouter véritablement mon enfant n’était pas à son comble. Nous communiquions à des niveaux différents. Je n’entendais pas l’expression de son ressenti, je voulais juste arrêter cette crise et être obéi.

J’ai renvoyé violemment à mon enfant l’injustice de son propos en argumentant piètrement qu’il fallait un peu garder les pieds sur terre, qu’on vit confortablement, sans avoir faim ni froid, avec de l’éducation et des loisirs, … bref, qu’on a une belle vie.

Ce n’est qu’après que j’ai compris que mon enfant exprimait sincèrement son ressenti et qu’il méritait d’être entendu et reconnu comme tel. J’ai également compris à quel point j’avais de la chance d’avoir un enfant qui parle de ses sentiments aussi clairement, sans peur d’être jugé ou incompris et que je n’avais pas le droit de l’en dégouter.

Plus généralement, pas juste pour un enfant, j’estime qu’un sentiment est une des choses les plus authentiques. On peut mentir en parole, avoir des actes sans intention ou conviction, mais un sentiment est obligatoirement authentique.

NB : Cet article est très brouillon. Je l’ai rédigé presque sans relecture, en pleine journée de travail. Je n’ai pas voulu reporter pour prendre le temps de peaufiner mon propos, j’ai préféré la spontanéité.

Publié dans Personnel | Laisser un commentaire

Deviens CTO

Il y a quelques jours, j’étais présent à /dev/var (une conférence locale, thématique web) et j’y ai rencontré une personne qui cherchait à rencontrer des personnes « techniques » pour la rejoindre dans une aventure d’entreprise naissante. C’était juste après mon intervention durant laquelle je parlais de mon enthousiasme dans mon travail et de certains aspects technologiques sur lesquels j’interviens.

Le lendemain soir, je reçois un mail sur ma boîte perso (mon adresse est facile à trouver, notamment à la fin de mes présentations). Le contenu de cette e-mail m’a fortement surpris. Lire la suite

Publié dans Personnel | Tagué | 9 commentaires

Retry after errors, with exponential backup (in Ruby)

There are situations where some errors can occur. Let’s say you connect to a remote service, like a database or an API over HTTP. An error raised by your client is not always permanent. It might be a network glitch or something else.

Here is an attempt (in Ruby) to retry on error, with a longer sleep time between attempts.

class WhateverException < StandardError; end
debug_counter = 0

sleep_times = [0.1, 0.2, 0.5, 1]
begin
    fail WhateverException, "counter=#{debug_counter += 1}"
rescue WhateverException
    if time = sleep_times[(nb_retries ||= 0)]
        sleep time
        puts "retry #{nb_retries} after #{time}s"
        nb_retries += 1
        retry
    else
        raise
    end
end

The 2 first lines are just context ; an exception class and a counter for debugging purposes.

sleep_times = [0.1, 0.2, 0.5, 1] is an array of times in seconds that I want to wait at each attempt.

The begin/rescue block allow to rescue the exception when it occurs, but also the retry (see later).

When an expected exception occurs, Ruby executes the body of the rescue part. It takes the first sleep time, wait that long, puts a debug line of text (that you'll want to remove or change to an audit log message), increments the number of attempts and executes the retry statement.

A retry statement rolls back to the previous begin block and executes it again, without any condition. That's why we have to deal with a maximum number of attempts or it will loop forever.

If we reach the end of the sleep_times array of times, Ruby will return nil and the if condition will fail. The original exception is raised again, as is.

Here is the output of this "script" :

ruby ~/tmp/retry.rb
retry 0 after 0.1s
retry 1 after 0.2s
retry 2 after 0.5s
retry 3 after 1s
/Users/jlecour/tmp/retry.rb:6:in `': counter: 5 (WhateverException)

Remember that in Ruby raise and fail are exactly the same method, but as Jim Weirich was saying :

Because I use exceptions to indicate failures, I almost always use the « fail » keyword rather than the « raise » keyword in Ruby. Fail and raise are synonyms so there is no difference except that « fail » more clearly communcates that the method has failed. The only time I use “raise” is when I am catching an exception and re-raising it, because here I’m not failing, but explicitly and purposefully raising an exception.

Publié dans Informatique, Personnel | Tagué | Laisser un commentaire

Rsync to just delete files on destination when missing from source

I have this situation where I have a huge number of images (about 50 millions, with 3-4 versions of each one), organized in a nested tree of directories, like images/103/045/475/example-{format}.jpg.

This immense catalog of images is replicated from our internal « master » to a CDN-like box. Sometimes, the replication is out of sync and some images a destroyed on the master but on the slave.

It’s not a surprise that Rsync has the right set of options to deal with this :

rsync --recursive --delete --ignore-existing --existing --prune-empty-dirs --verbose src/ dst/

Let me explain each option.

--recursive will explore the whole directory tree, not just the first level.

--delete will remove files in dst that are not in src.

--ignore-existing will not update any file in dst

--existing will not create any file in dst.

--prune-empty-dirs will remove empty directories in dst, not just deleting files.

--verbose will log what it does.

By not trying to compare the files, it’s much faster, but of course it’s only cleanup, not a real synchronization.

You can also run this a first time with --dry-run to print each action instead of executing them, to verify that Rsync does what you want.

The complete list of options is available in the man page

Publié dans Informatique | Tagué | 1 commentaire