diff --git a/VERSIONS b/VERSIONS
index 90bb5d3099627ffca7f7824f18a0373e60b5ef97..2c115a558b685e30000b71626d8fad42b3249105 100644
--- a/VERSIONS
+++ b/VERSIONS
@@ -1,3 +1,20 @@
+28/10/2016 - v7.7.14
+
+ - ticket #49215 : Fiche bibliothèque : correction de l'affichage des horaires passées
+
+ - ticket #48232 : Article : prise en charge de la nouvelle API Facebook
+
+ - ticket #47546 : Administration : correction du message en cours quand l'envoi de la newsletter a échoué
+
+ - ticket #49590 : Administration : correction de l'envoi de newsletter et ajout d'une gestion d'erreur
+
+ - ticket #47906 : Authentification : correction de l'authentification des abonnés SIGB qui ont des id_sigb identiques dans un réseau
+
+ - ticket #43916 : Recherche : tri par ordre alphabétique des favoris.
+
+ - ticket #49353 : Administration : bloquage de l'indexation du bloque ajax CVS dans la notice par les bots
+
+
 25/10/2016 - v7.7.12 - v7.7.13
 
  - ticket #47780 : correction du comportement d'écriture de la classe i18n lors de la migration de plugin
diff --git a/application/modules/admin/controllers/NewsletterController.php b/application/modules/admin/controllers/NewsletterController.php
index 08c7e239fb23c1adec4f904d59aec3b62305e563..95fddc32a97a5af45ae265faeb4f4b4030c0e353 100644
--- a/application/modules/admin/controllers/NewsletterController.php
+++ b/application/modules/admin/controllers/NewsletterController.php
@@ -44,7 +44,23 @@ class Admin_NewsletterController extends ZendAfi_Controller_Action {
                                     return $this->view->tag('span', $this->view->boutonIco("picto=users", "bulle=" . $this->_('Inscrits')) . $model->getNumberOfUsers(), ['style' => 'white-space:nowrap']);
                                   }],
                                 ['action' => 'sendtest', 'content' => $this->view->boutonIco('type=test')],
-                                ['action' => 'send', 'content' => $this->view->boutonIco('type=mail')],
+                                ['action' => 'send',
+                                 'content' => function($model) {
+                                    Class_ScriptLoader::getInstance()->addJQueryReady("
+function sendNewsletterClick(event) {
+  var target = $(event.target).closest('a');
+  var answer = confirm(\"".$this->_("Envoyer la lettre d'information ?")."\");
+  if (answer == false) {
+    event.preventDefault();
+    return;
+  }
+}
+
+  $(\"a[rel='send']\").click(sendNewsletterClick);
+");
+
+
+                                    return $this->view->boutonIco('type=mail');}],
                                 ['action' => 'duplicate', 'content' => $this->view->boutonIco('type=duplicate')],
                                 ['action' => 'delete', 'content' => $this->view->boutonIco('type=del')],
             ],
@@ -96,8 +112,13 @@ class Admin_NewsletterController extends ZendAfi_Controller_Action {
 
 
   public function sendAction() {
-    if ($newsletter = Class_Newsletter::find((int)$this->_getParam('id')))
-      $newsletter->send();
+    if (!$newsletter = Class_Newsletter::find((int)$this->_getParam('id'))) {
+      $this->_helper->notify($this->_('Envoi impossible : Newsletter inconnue'));
+      return $this->_redirectToIndex();
+    }
+
+    if (!$newsletter->send())
+      $this->_helper->notify($this->_('Envoi impossible : erreur à la création de la commande d\'envoi'));
 
     $this->_redirectToIndex();
   }
@@ -256,4 +277,12 @@ class Admin_NewsletterController extends ZendAfi_Controller_Action {
                                        'id' => $model->getId()], null, true),
                      ['prependBase' => false]);
   }
+
+
+  public function showStatusAction() {
+    $this->view->titre = $this->_('Détails de l\'erreur');
+    $this->view->model = Class_Newsletter_Dispatch::find((int)$this->_getParam('id'));
+    if (!$this->view->model)
+      throw new Zend_Controller_Action_Exception($this->_('Désolé, cette page n\'existe pas'), 404);
+  }
 }
\ No newline at end of file
diff --git a/application/modules/admin/views/scripts/newsletter/_newsletter_list_item.phtml b/application/modules/admin/views/scripts/newsletter/_newsletter_list_item.phtml
deleted file mode 100644
index 9f07a3179d63cab9ced4686444deba5e81d134b5..0000000000000000000000000000000000000000
--- a/application/modules/admin/views/scripts/newsletter/_newsletter_list_item.phtml
+++ /dev/null
@@ -1,17 +0,0 @@
-<li>
- <span class='titre'>
-   <?php 
-     echo $this->tagAnchor($this->url(array('action' => 'edit',
-                                            'id' => $this->newsletter->getId())), 
-                           htmlentities($this->newsletter->getTitre(), ENT_QUOTES, 'UTF-8'));
-
-    foreach(array('delete' => 'Supprimer', 'send' => 'Envoyer') 
-            as $action => $label) {
-      echo ' - ';
-      echo $this->tagAnchor($this->url(array('action' => $action,
-                                             'id' => $this->newsletter->getId())),
-                            $label);
-    }
-   ?>
- </span>
-</li>
\ No newline at end of file
diff --git a/application/modules/admin/views/scripts/newsletter/_newsletter_row.phtml b/application/modules/admin/views/scripts/newsletter/_newsletter_row.phtml
deleted file mode 100644
index 7c30af706f590243259be68956d72b02e306b8ec..0000000000000000000000000000000000000000
--- a/application/modules/admin/views/scripts/newsletter/_newsletter_row.phtml
+++ /dev/null
@@ -1,20 +0,0 @@
-<tr class="<?php echo $this->item_class ?>">
-  <td><?php echo htmlentities($this->newsletter->getTitre(), ENT_QUOTES, 'UTF-8'); ?></td>
-  <td><?php echo $this->tagProgressBarForNewsletter($this->newsletter);?></td>
-  <?php
-  foreach(['edit' => $this->boutonIco("type=edit"),
-           'delete' => $this->boutonIco("type=del"),
-           'preview' => $this->boutonIco("type=show"),
-           'duplicate' => $this->boutonIco("type=duplicate"),
-           'edit-subscribers' => $this->boutonIco("picto=users", "bulle=Membres").$this->newsletter->getNumberOfUsers(),
-           'sendtest' => $this->boutonIco("type=test"),
-           'send' => $this->boutonIco("type=mail")]
-          as $action => $view) {
-    echo "<td rel='$action'>";
-    echo $this->tagAnchor($this->url(['action' => $action,
-                                      'id' => $this->newsletter->getId()]),
-                          $view);
-    echo '</td>';
-  }
-  ?>
-</tr>
diff --git a/application/modules/admin/views/scripts/newsletter/index.phtml b/application/modules/admin/views/scripts/newsletter/index.phtml
index 3148679ac513c80a5a1ad8f7a701846849910e3c..cfb7cc73d7b2500d5fd26007523eca43d66d6236 100644
--- a/application/modules/admin/views/scripts/newsletter/index.phtml
+++ b/application/modules/admin/views/scripts/newsletter/index.phtml
@@ -15,18 +15,3 @@ echo $this->tagModelTable($this->newsletters,
 if (isset($this->subview))
   echo $this->tag('div', $this->subview, ['class' => 'subview']);
 ?>
-
-<script type="text/javascript">
-function sendNewsletterClick(event) {
-  var target = $(event.target).closest('a');
-  var answer = confirm("<?php echo $this->_("Envoyer la lettre d'information ?"); ?>");
-  if (answer == false) {
-    event.preventDefault();
-    return;
-  }
-}
-
-$(document).ready(function() {
-  $("td[rel='send'] a").click(sendNewsletterClick);
-});
-</script>
diff --git a/application/modules/admin/views/scripts/newsletter/show-status.phtml b/application/modules/admin/views/scripts/newsletter/show-status.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..bd088d639a92460e982fc092562f19475b2445ff
--- /dev/null
+++ b/application/modules/admin/views/scripts/newsletter/show-status.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->tag('p', nl2br($this->model->getErrorMessage()));
diff --git a/application/modules/opac/controllers/NoticeajaxController.php b/application/modules/opac/controllers/NoticeajaxController.php
index 3a4c9a5df19a34ae5ad3f5b4c4a9e06d1004832d..7c2fd4fc152efdb53fea0fe82794668f5125c599 100644
--- a/application/modules/opac/controllers/NoticeajaxController.php
+++ b/application/modules/opac/controllers/NoticeajaxController.php
@@ -470,6 +470,10 @@ class NoticeAjaxController extends Zend_Controller_Action {
 
 
   function cvsSearchAction() {
+    if ((new Class_UserAgent())->isBot())
+      return $this->_sendResponse('');
+
+
     $this->preferences = Class_Profil::getCurrentProfil()->getCfgModulesPreferences('recherche',
                                                                                     'resultat',
                                                                                     'simple');
diff --git a/application/modules/opac/views/scripts/head.phtml b/application/modules/opac/views/scripts/head.phtml
index 9f93a56d7b1a8d4681b24456275f21ce06601176..bb0f428ecc23a1c4ccabde2064e67c8d387c2edd 100644
--- a/application/modules/opac/views/scripts/head.phtml
+++ b/application/modules/opac/views/scripts/head.phtml
@@ -1,44 +1,49 @@
 <!DOCTYPE html>
 <html lang="<?php echo $this->_translate()->getLocale() ?>">
-    <head>
+  <head>
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
     <title><?php echo $this->titre ?></title>
-<?php $current_profil = Class_Profil::getCurrentProfil(); ?>
+    <?php $current_profil = Class_Profil::getCurrentProfil(); ?>
 
     <meta name="description" content="<?php echo $current_profil->getCommentaire();?>" />
     <meta name="keywords" content="<?php echo $current_profil->getRefTags();?>" />
     <meta content="all" name="robots" />
     <meta content="10 days" name="revisit-after" />
     <meta content="width=device-width, initial-scale=1, maximun-scale=1" name="viewport" />
-<?php
+    <?php
     Class_ScriptLoader::getInstance()->loadMeta();
 
-echo $current_profil->getStyleCss();
-if ($current_profil->hasFavicon())
-  echo sprintf('<link rel="shortcut icon" href="%s"/>', $current_profil->getFavicon());
-
-$head_scripts = Class_ScriptLoader::newInstance()
-  ->loadJQuery()
-  ->loadJQueryUI()
-  ->addOPACStyleSheet('global')
-  ->addSkinStyleSheets(['global', 'erreur', 'dialog', 'popup', 'nuage_tags', 'bib'])
-  ->addAdminStyleSheet('subModal')
-  ->addOPACStyleSheet('print', ['media' => 'print'])
-  ->addInlineScript(sprintf('var baseUrl="%s"; var imagesUrl="%s"; var cssUrl="%s"; var userFilesUrl="%s"',
-                            BASE_URL, URL_IMG, URL_CSS, USERFILESURL))
-
-  ->addAdminScripts(['onload_utils', 'global', 'toolbar', 'common'])
-  ->addOPACScripts(['abonne', 'menu', 'bib', 'avis', 'recherche',
-                    'jquery.placeholder.min', 'accessibility', 'subModal',
-                    'division-five', 'reload_module'])
-  ->addJQueryReady('
-       $("input").placeholder();
-       autoHideShowConfigurationModule();
-       initializeNoticeMurAnimation();
-       initializeImgHover();
-       initializePopups();
-       initializeDivisionFive();
-       initializeReloadModule();')
+    echo $current_profil->getStyleCss();
+    if ($current_profil->hasFavicon())
+      echo sprintf('<link rel="shortcut icon" href="%s"/>', $current_profil->getFavicon());
+
+    $head_scripts = Class_ScriptLoader::newInstance()
+                        ->loadJQuery()
+                        ->loadJQueryUI()
+                        ->addOPACStyleSheet('global')
+                        ->addSkinStyleSheets(['global', 'erreur', 'dialog', 'popup', 'nuage_tags', 'bib'])
+                        ->addAdminStyleSheet('subModal')
+                        ->addOPACStyleSheet('print', ['media' => 'print'])
+                        ->addInlineScript(sprintf('var baseUrl="%s"; var imagesUrl="%s"; var cssUrl="%s"; var userFilesUrl="%s"',
+                                                  BASE_URL, URL_IMG, URL_CSS, USERFILESURL))
+
+                        ->addAdminScripts(['onload_utils', 'global', 'toolbar', 'common'])
+                        ->addOPACScripts(['abonne',
+                                          'menu',
+                                          'bib',
+                                          'avis',
+                                          'recherche',
+                                          'accessibility',
+                                          'subModal',
+                                          'division-five',
+                                          'reload_module'])
+                        ->addJQueryReady('
+                                         autoHideShowConfigurationModule();
+                                         initializeNoticeMurAnimation();
+                                         initializeImgHover();
+                                         initializePopups();
+                                         initializeDivisionFive();
+                                         initializeReloadModule();')
   ->showNotifications();
 
 $this->adminTools();
@@ -103,11 +108,11 @@ foreach([7,8] as $ie)
                                         ['ie_version' => $ie]);
 $script_loader->addJQueryReady('setupAnchorsTarget();');
 
-$head_scripts->renderStyleSheets();
-$script_loader->renderStyleSheets();
+    $head_scripts->renderStyleSheets();
+    $script_loader->renderStyleSheets();
 
-$head_scripts->renderJavaScripts();
-$script_loader->renderJavaScripts();
-echo $this->heartbeat();
-?>
-</head>
+    $head_scripts->renderJavaScripts();
+    $script_loader->renderJavaScripts();
+    echo $this->heartbeat();
+                         ?>
+  </head>
diff --git a/application/modules/opac/views/scripts/portail.phtml b/application/modules/opac/views/scripts/portail.phtml
index 475652bbe572c823e28d700e63b9d14b8e3e7b52..27c6e6dc9b16613fb2bf9b54331801b6d583e27d 100644
--- a/application/modules/opac/views/scripts/portail.phtml
+++ b/application/modules/opac/views/scripts/portail.phtml
@@ -1,7 +1,7 @@
 <?php
 ob_start();
 echo '<body '.$this->bodyParam.'>';
-if (isTelephone() && Class_Profil::isAPhoneProfilEnabled())
+if ((new Class_UserAgent())->isMobile() && Class_Profil::isAPhoneProfilEnabled())
   echo sprintf('<div class="back_to_phone">%s</div>',
                  $this->tagAnchor($this->url(['module' => 'telephone'], null, true),
                                   $this->_('Afficher le site en mode mobile')));
diff --git a/cosmogramme/php/_init.php b/cosmogramme/php/_init.php
index b567a79e5386d5ee557ad2581425fc6778956231..5842c641e270bb809fa84d2d297cc099f2cd6173 100644
--- a/cosmogramme/php/_init.php
+++ b/cosmogramme/php/_init.php
@@ -1,7 +1,7 @@
 <?php
 error_reporting(E_ERROR | E_PARSE);
 
-define("PATCH_LEVEL","312");
+define("PATCH_LEVEL","313");
 
 define("APPLI","cosmogramme");
 define("COSMOPATH", "/var/www/html/vhosts/opac2/www/htdocs");
diff --git a/cosmogramme/php/classes/classe_abonne.php b/cosmogramme/php/classes/classe_abonne.php
index bbd292c9ee5bd4f40b1ff96a33945aa5051706c1..19cf19ed34b6ee6f4c8e60cf69199dd67f8e414c 100644
--- a/cosmogramme/php/classes/classe_abonne.php
+++ b/cosmogramme/php/classes/classe_abonne.php
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 /**
  * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
  *
@@ -101,7 +101,7 @@ class abonne
 
     if (isset($enreg['MAIL']))
       $enreg['MAIL'] = $this->clean_email($enreg['MAIL']);
-		$this->saveorUpdateInDB($enreg);
+		$this->saveOrUpdateInDB($enreg);
 	}
 
 
@@ -145,8 +145,11 @@ class abonne
 
 
 	protected function saveOrUpdateInDB($data){
-		if(!$user = $this->findMatchingUserInDB($data))
-			$user= new Class_Users();
+    $new_user = Class_Users::newInstance($data);
+    $bib = Class_IntBib::find($this->id_bib);
+
+		if(!$user = Class_Users::findMatchingPatron($new_user,$bib))
+      $user = $new_user;
 
 		$user
 			->updateAttributes($data)
@@ -156,33 +159,17 @@ class abonne
 	}
 
 
-	protected function findMatchingUserInDB($data) {
-		if (isset($data['ID_SIGB'])
-				&& ($data['ID_SIGB'])
-				&& ($user = Class_Users::findFirstBy(['id_sigb' => $data['ID_SIGB']])))
-			return $user;
-
-		$params_existing_user = ['login'=>$data["LOGIN"],
-														 'id_site'=>$data['ID_SITE']];
-
-		if (isset($data['ORDREABON']))
-			$params_existing_user['ordreabon'] = $data['ORDREABON'];
-
-		return Class_Users::findFirstBy($params_existing_user);
-	}
-
-
 	private function changeAccents($chaine)
 	{
 		if(!trim($chaine)) return $chaine;
 		switch($this->type_accents)
 			{
-			case 2: // Windows
-				return utf8_encode($chaine);
-			case 3: // Dos
-				for($i=0; $i < strlen($chaine); $i++) $new.=$this->dosDecode($chaine[$i]);
-				return utf8_encode($new);
-			default: return $chaine;
+        case 2: // Windows
+          return utf8_encode($chaine);
+        case 3: // Dos
+          for($i=0; $i < strlen($chaine); $i++) $new.=$this->dosDecode($chaine[$i]);
+          return utf8_encode($new);
+        default: return $chaine;
 			}
 	}
 
@@ -191,28 +178,27 @@ class abonne
 	{
 		switch($char)
 			{
-			case 0xe9: $result = 'é';	break ;
-			case 0xe8: $result = 'è';	break ;
-			case 0xeb: $result = 'ë';	break ;
-			case 0xe4: $result = 'ä';	break ;
-			case 0xe2: $result = 'â';	break ;
-			case 0xef: $result = 'ï';	break ;
-			case 0xcf: $result = 'Ï';	break ;
-			case 0xee: $result = 'î';	break ;
-			case 0xce: $result = 'ÃŽ';	break ;
-			case 0xf4: $result = 'ô';	break ;
-			case 0xf6: $result = 'ö';	break ;
-			case 0xd6: $result = 'Ö';	break ;
-			case 0xfc: $result = 'ü';	break ;
-			case 0xdc: $result = 'Ü';	break ;
-			case 0xfb: $result = 'û';	break ;
-			case 0xe7: $result = 'ç';	break ;
-			case 0xc7: $result = 'Ç';	break ;
-			default: $result = $char;	break;
+        case 0xe9: $result = 'é';	break ;
+        case 0xe8: $result = 'è';	break ;
+        case 0xeb: $result = 'ë';	break ;
+        case 0xe4: $result = 'ä';	break ;
+        case 0xe2: $result = 'â';	break ;
+        case 0xef: $result = 'ï';	break ;
+        case 0xcf: $result = 'Ï';	break ;
+        case 0xee: $result = 'î';	break ;
+        case 0xce: $result = 'ÃŽ';	break ;
+        case 0xf4: $result = 'ô';	break ;
+        case 0xf6: $result = 'ö';	break ;
+        case 0xd6: $result = 'Ö';	break ;
+        case 0xfc: $result = 'ü';	break ;
+        case 0xdc: $result = 'Ü';	break ;
+        case 0xfb: $result = 'û';	break ;
+        case 0xe7: $result = 'ç';	break ;
+        case 0xc7: $result = 'Ç';	break ;
+        default: $result = $char;	break;
 			}
 		return $result;
 	}
-
 }
 
 ?>
\ No newline at end of file
diff --git a/cosmogramme/sql/patch/patch_313.php b/cosmogramme/sql/patch/patch_313.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f2409fa08ce6770a38fac39ff6023c1b3f5fc22
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_313.php
@@ -0,0 +1,5 @@
+<?php
+try {
+  Zend_Db_Table_Abstract::getDefaultAdapter()
+    ->query('alter table newsletter_dispatch add error longtext');
+} catch(Exception $e) {}
\ No newline at end of file
diff --git a/cosmogramme/tests/php/classes/AbonneIntegrationTest.php b/cosmogramme/tests/php/classes/AbonneIntegrationTest.php
index 4e75bbe0df6483827ed5a92a3adf5c103b4fec6f..56d375400ac89a1019d402406e999add8b160eea 100644
--- a/cosmogramme/tests/php/classes/AbonneIntegrationTest.php
+++ b/cosmogramme/tests/php/classes/AbonneIntegrationTest.php
@@ -25,11 +25,16 @@ require_once 'ModelTestCase.php';
 
 
 abstract class AbonneIntegrationTestCase extends ModelTestCase {
-	protected $abon_config;
+	protected $abon_config,
+    $_storm_default_to_volatile = true;
 
 	public function setup(){
 		parent::setup();
-		Storm_Model_Loader::defaultToVolatile();
+
+    $this->fixture('Class_IntBib',
+                   ['id' => 2,
+                    'id_bib' => 2]);
+
 		$this->abon_config = new abonne();
 		$this->abon_config->setIdBib(2);
 	}
@@ -221,6 +226,8 @@ class AbonneIntegrationASCIIWithRoutoInDbTest extends AbonneIntegrationTestCase
 																			 ['id' => 5,
 																				'nom'=>'Routo',
 																				'prenom'=>'Pierre',
+                                        'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                                        'idabon' => '123456',
 																				'login'=>'5  5',
 																				'id_site' => 2,
 																				'pseudo' => 'riri',
@@ -445,7 +452,7 @@ class AbonneIntegrationXMLWithRoutoInDbTest extends AbonneIntegrationXMLTestCase
 		$this->abon_config->importFicheXml($champs_sigb_xml);
 
 		$this->zozio_after_import = Class_Users::findFirstby(['login'=>'0100 3080',
-																													 'id_site' => 2]);
+                                                          'id_site' => 2]);
 	}
 
 	/** @test **/
diff --git a/index.php b/index.php
index 9f3e7136edd15d23694c711d2916b1ca247401f2..5698f917f75f1e88ec486df0ac8ce3d12eca2828 100644
--- a/index.php
+++ b/index.php
@@ -27,7 +27,8 @@ if ((!file_exists('local.php') || !file_exists('config.ini') || !file_exists('co
 require('includes.php');
 
 try {
-	if (isUserAgentBotAndNotAllowed())
+  require_once(__DIR__ . '/library/Class/UserAgent.php');
+	if ((new Class_UserAgent())->isBotAndNotAllowed())
 		exit;
 
 	setupOpac()->dispatch();
diff --git a/library/Class/Batch/SendNewsletters.php b/library/Class/Batch/SendNewsletters.php
index 5da4957ddde360faf27c310ec09b3a9fa664f8d4..d20de23df186ef0af68fc4fa096ecb866c2830ad 100644
--- a/library/Class/Batch/SendNewsletters.php
+++ b/library/Class/Batch/SendNewsletters.php
@@ -55,16 +55,36 @@ class Class_Batch_SendNewsletters extends Class_Batch_Abstract {
 
 
   public function run() {
-    $this->getCommand()
-         ->exec('/usr/bin/php -f '
-                . realpath(dirname(__FILE__)) . '/../../../scripts/sendNewsletter.php '
-                . $this->getExecParams()
-                . ' > /dev/null &');
+    $shell = '/usr/bin/php -f '
+      . realpath(dirname(__FILE__)) . '/../../../scripts/sendNewsletter.php '
+      . $this->getExecParams()
+      . ' > /dev/null & echo $!';
+
+    $command = $this->getCommand();
+    $command->exec($shell);
+    if ((!$output = $command->getOutput())
+        || !is_array($output)
+        || !$output[0]) {
+      $this->_endWithError('Unable to run ' . $shell);
+      return false;
+    }
 
-    return $this;
+    $command->exec('ps '. $output[0]);
+
+    if (!$success = 0 === $command->getReturnVar())
+      $this->_endWithError('Unable to run ' . $shell);
+
+    return $success;
   }
 
 
+  protected function _endWithError($message) {
+    $this->_dispatch
+      ->setError(json_encode(['message' => $message]))
+      ->beEnded()
+      ;
+  }
+
 
   public function sendOne($email) {
     if (!$this->_dispatch)
@@ -136,6 +156,11 @@ class Class_Batch_SendNewsletters extends Class_Batch_Abstract {
   }
 
 
+  protected function _checksum($params) {
+    return sha1(serialize($params));
+  }
+
+
   public function getTimeLimit() {
     if (!$this->_time_limit)
       $this->_time_limit = Class_Systeme_TimeLimit::getInstance();
diff --git a/library/Class/MoteurRecherche/Facettes.php b/library/Class/MoteurRecherche/Facettes.php
index 63bd29bcda2772df9eefb3201636c1b0b0fb06c3..aa3766189ab73051819d554cdd8aaf9e8c5ad3d2 100644
--- a/library/Class/MoteurRecherche/Facettes.php
+++ b/library/Class/MoteurRecherche/Facettes.php
@@ -179,7 +179,11 @@ class Class_MoteurRecherche_Facettes {
     $codification = Class_Codification::getInstance();
     $matches = [];
 
-    $user_bookmarks_codes = array_map(function($model) {return $model->getFacet();}, $user_bookmarks);
+    $user_bookmarks_codes = array_map(function($model)
+                                      {
+                                        return $model->getFacet();
+                                      },
+                                      $user_bookmarks);
 
     foreach($user_bookmarks_codes as $bookmark_code) {
       if(!isset($codes_count[$bookmark_code]))
@@ -192,9 +196,17 @@ class Class_MoteurRecherche_Facettes {
         continue;
 
       $matches[] = ['id' => $bookmark_code,
-                    'label' => $facet->getCodeRubriqueLibelle() . ' : ' . $facet_label . ' (' . $codes_count[$bookmark_code] . ')'];
+                    'label' => sprintf('%s : %s (%d)',
+                                       $facet->getCodeRubriqueLibelle(),
+                                       $facet_label,
+                                       $codes_count[$bookmark_code])];
     }
 
+    usort($matches, function($first, $second)
+         {
+           return strcmp($first['label'], $second['label']);
+         });
+
     return $matches;
   }
 
diff --git a/library/Class/Newsletter.php b/library/Class/Newsletter.php
index be96e8d56dc275005074b22d06e0d73d5c5540a1..c030c17eac4689748eedb57bb813aa73e5eaf63a 100644
--- a/library/Class/Newsletter.php
+++ b/library/Class/Newsletter.php
@@ -99,10 +99,15 @@ class Class_Newsletter extends Storm_Model_Abstract {
 
 
   public function send() {
-    if (!$dispatch = $this->getLastDispatchInProgress())
-      $dispatch = Class_Newsletter_Dispatch::newFrom($this);
+    return (new Class_Batch_SendNewsletters($this->_getOrCreateDispatchToRun()))
+      ->run();
+  }
+
 
-    return (new Class_Batch_SendNewsletters($dispatch))->run();
+  protected function _getOrCreateDispatchToRun() {
+    return ($dispatch = $this->getLastDispatch()) && $dispatch->isRunnable()
+      ? $dispatch->resetError()
+      : Class_Newsletter_Dispatch::newFrom($this);
   }
 
 
@@ -404,11 +409,17 @@ class Class_Newsletter extends Storm_Model_Abstract {
     return $this->getTitre();
   }
 
+
   public function getLastDispatchInProgress() {
     return Class_Newsletter_Dispatch::lastDispatchInProgressFor($this);
   }
 
 
+  public function getLastDispatch() {
+    return Class_Newsletter_Dispatch::lastDispatchFor($this);
+  }
+
+
   public function getSortedRecipientsByDedicatedAndLabel() {
     $groups = $this->getRecipientsGroups();
     usort(
diff --git a/library/Class/Newsletter/Dispatch.php b/library/Class/Newsletter/Dispatch.php
index 9cd117e1f585348e83e5c5195ded5e1e875662a7..151d7139f9c3c8e9c225f8f25264e41bfd1cdee9 100644
--- a/library/Class/Newsletter/Dispatch.php
+++ b/library/Class/Newsletter/Dispatch.php
@@ -46,6 +46,13 @@ class Newsletter_DispatchLoader extends Storm_Model_Loader {
                                                    'ended_on' => null]);
 
   }
+
+
+  public function lastDispatchFor($newsletter) {
+    return Class_Newsletter_Dispatch::findFirstBy(['newsletter_id' => $newsletter->getId(),
+                                                   'order' => 'created_on desc']);
+
+  }
 }
 
 
@@ -69,7 +76,8 @@ class Class_Newsletter_Dispatch extends Storm_Model_Abstract {
                           'users' => ['through' => 'dispatch_users']];
 
   protected $_default_attribute_values = ['collected' => 0,
-                                          'ended_on' => null];
+                                          'ended_on' => null,
+                                          'error' => null];
 
 
   public function beforeSave() {
@@ -114,10 +122,10 @@ class Class_Newsletter_Dispatch extends Storm_Model_Abstract {
       $params = ['dispatch_id' => $this->getId(),
                  'user_id' => $model->getId()];
 
-      if (Class_Newsletter_DispatchUser::findFirstBy($params))
-        return;
-      if (in_array( $model->getMail(),$blacklist_mails))
+      if (Class_Newsletter_DispatchUser::findFirstBy($params)
+          || in_array($model->getMail(), $blacklist_mails))
         return;
+
       Class_Newsletter_DispatchUser::newInstance(['dispatch_id' => $this->getId(),
                                                   'user_id' => $model->getId(),
                                                   'mail' => $model->getMail()])
@@ -146,4 +154,44 @@ class Class_Newsletter_Dispatch extends Storm_Model_Abstract {
     $this->getNewsletter()->setLastDistributionDateWithFormat();
     return $this;
   }
+
+
+  public function getErrorMessage() {
+    if (!$this->hasError()
+        || (!$json = json_decode($this->getError(), true))
+        || !array_key_exists('message', $json))
+      return '';
+
+    return $json['message'];
+  }
+
+
+  public function isRunnable() {
+    return $this->isInProgress() || $this->canRetry();
+  }
+
+
+  public function canRetry() {
+    return $this->hasError()
+      && $this->getCollected()
+      && $this->numberOfDoneUsers() < $this->numberOfDispatchUsers();
+  }
+
+
+  public function isInProgress() {
+    return !$this->hasEndedOn();
+  }
+
+
+  public function resetError() {
+    if (!$this->canRetry())
+      return $this;
+
+    $this
+      ->setError(null)
+      ->setEndedOn(null)
+      ->save();
+
+    return $this;
+  }
 }
diff --git a/library/Class/Ouverture/Visitor.php b/library/Class/Ouverture/Visitor.php
index 1f95fa425ded98f0f591b076a83b2da1c1f3a3b1..f4293403362421a2ff17aa230b80260458d8c46c 100644
--- a/library/Class/Ouverture/Visitor.php
+++ b/library/Class/Ouverture/Visitor.php
@@ -119,7 +119,7 @@ class Class_Ouverture_Visitor {
     }
 
     return $opening->isValidOnDate($now)
-      || $now_plus_30 >= strtotime($opening->getValidityStart());
+      || $opening->isValidOnDate($now_plus_30);
   }
 
 
diff --git a/library/Class/UserAgent.php b/library/Class/UserAgent.php
new file mode 100644
index 0000000000000000000000000000000000000000..e0421a8ab34966cea22df3c4810ca63b4404e02b
--- /dev/null
+++ b/library/Class/UserAgent.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_UserAgent {
+  protected
+    $_user_agent_string;
+
+  public function __construct($useragent = null) {
+    $this->_user_agent_string = $useragent
+      ? $useragent
+      : $this->_readUserAgentString();
+  }
+
+
+  public function isBot() {
+    return 0 !== preg_match('/(bot\/|sistrix|voilabot|slurp)/i',
+                            $this->_user_agent_string);
+  }
+
+
+  protected function _readUserAgentString() {
+    return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
+  }
+
+
+  public function getUserAgentString() {
+    return $this->_user_agent_string;
+  }
+
+
+  public function isBotAndNotAllowed() {
+    defineConstant('BOT_ALLOWED_START_HOUR', 20);
+    defineConstant('BOT_ALLOWED_END_HOUR', 8);
+
+    return $this->isBotAndNotAllowedBetweenHours(BOT_ALLOWED_START_HOUR,
+                                                 BOT_ALLOWED_END_HOUR);
+  }
+
+
+  public function isBotAndNotAllowedBetweenHours($start_hour, $end_hour) {
+    if (!$this->isBot())
+      return false;
+
+    $now = time();
+    $start = mktime($start_hour, 0, 0);
+    $end = mktime($end_hour, 0, 0);
+
+    return ($now < $start) && ($now > $end);
+  }
+
+
+  public function isMobile() {
+    if (!$this->_user_agent_string)
+      return false;
+
+    $regex_match="/(mobile|nokia|iphone|android|motorola|^mot\-|softbank|foma|docomo|kddi|up\.browser|up\.link|";
+    $regex_match.="htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|";
+    $regex_match.="blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam\-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|";
+    $regex_match.="symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte\-|longcos|pantech|gionee|^sie\-|portalmmm|";
+    $regex_match.="jig\s browser|hiptop|^ucweb|^benq|haier|^lct|opera\s*mobi|opera\*mini|320x320|240x320|176x220";
+    $regex_match.=")/i";
+
+    return (isset($_SERVER['HTTP_X_WAP_PROFILE']) or isset($_SERVER['HTTP_PROFILE'])
+            or preg_match($regex_match, strtolower($this->_user_agent_string)));
+  }
+}
+?>
\ No newline at end of file
diff --git a/library/Class/Users.php b/library/Class/Users.php
index 14a01ab2ed380ba75eb0ccbc7fc9bd332bded19c..8a17e0f4ff06d13207616c0c07b183b54402ac99 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -43,6 +43,34 @@ class UsersLoader extends Storm_Model_Loader {
   }
 
 
+  public function findMatchingPatron($patron, $bib) {
+    if (!$patron || !$bib)
+      return null;
+
+    if (($ordreabon = $patron->getOrdreabon())
+         && ($user = Class_Users::findFirstBy(['login' => $patron->getLogin(),
+                                               'ordreabon' => $ordreabon,
+                                               'id_site' => $bib->getIdBib()])))
+      return $user;
+
+    if ($user = Class_Users::findFirstBy(['login' => $patron->getLogin(),
+                                          'id_sigb' => $patron->getIdSigb(),
+                                          'id_site' => $bib->getIdBib()]))
+      return $user;
+
+    if ($user = Class_Users::findFirstBy(['login' => $patron->getLogin(),
+                                          'id_site' => $bib->getIdBib()]))
+      return $user;
+
+    if (($id_sigb = $patron->getIdSigb())
+        && ($user = Class_Users::findFirstBy(['id_site' => $bib->getIdBib(),
+                                              'id_sigb' => $patron->getIdSigb()])))
+      return $user;
+
+    return null;
+  }
+
+
   public function findAllLike($search, $by_right = null, $limit = 500) {
     $sql_template = 'select bib_admin_users.* from bib_admin_users ';
 
@@ -1720,7 +1748,8 @@ class Class_Users extends Storm_Model_Abstract {
 
 
   public function getBookmarks() {
-    return array_filter(array_merge($this->getBookmarkedDomains(), $this->getBookmarkedLibraries()));
+    return array_filter(array_merge($this->getBookmarkedDomains(),
+                                          $this->getBookmarkedLibraries()));
   }
 
 
diff --git a/library/ZendAfi/Auth/Adapter/CommSigb.php b/library/ZendAfi/Auth/Adapter/CommSigb.php
index 490508be9d0dc7dfe5338fbebb2880bf20f88533..3e478cbb1dce8c5118d01d1315162e08199c9870 100644
--- a/library/ZendAfi/Auth/Adapter/CommSigb.php
+++ b/library/ZendAfi/Auth/Adapter/CommSigb.php
@@ -16,13 +16,14 @@
  *
  * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
  * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
 class ZendAfi_Auth_Adapter_CommSigb implements Zend_Auth_Adapter_Interface {
   protected $_identity = null;
   protected $_credential = null;
   protected $_authenticated_user = null;
+  protected $_bib = null;
 
 
   /**
@@ -49,39 +50,36 @@ class ZendAfi_Auth_Adapter_CommSigb implements Zend_Auth_Adapter_Interface {
    * @return Zend_Auth_Result
    */
   public function authenticate() {
-    return 
+    return
       $this->authResult(
-        $this->matchingProcessSIGBUserInDB(
-          $this->getUserFromSigb(Class_Users::newInstance(['login' => $this->_identity,
-                                                           'password' => $this->_credential]))));
+                        $this->matchingProcessSIGBUserInDB(
+                                                           $this->getUserFromSigb(Class_Users::newInstance(['login' => $this->_identity,
+                                                                                                            'password' => $this->_credential]))));
   }
-  
+
 
   public function matchingProcessSIGBUserInDB($user_from_sigb) {
     if(!$user_from_sigb)
       return null;
 
-    $users_in_db = [];
+    return $this->_getUserToSave($user_from_sigb)
+                ->beAbonneSIGB()
+                ->setLogin($this->_identity)
+                ->setPassword($this->_credential)
+                ->updateUser($user_from_sigb);
+  }
+
 
-    if ($user_from_sigb->getIdSigb())
-      $users_in_db = Class_Users::findAllBy(['id_sigb' => $user_from_sigb->getIdSigb()]);
-    if (count($users_in_db) != 1) {
-      $users_in_db = Class_Users::findAllBy(['login' => $user_from_sigb->getLogin()]);
-      if(1 < count($users_in_db))
-        $users_in_db = Class_Users::findAllBy(['login' => $user_from_sigb->getLogin(),
-                                               'id_sigb' => $user_from_sigb->getIdSigb()]);
-    }
+  protected function _getUserToSave($user_from_sigb) {
+    $new_user = Class_Users::newInstance()->setLogin($this->_identity);
 
-    if (1 == count($users_in_db))
-      $user_to_save = $users_in_db[0];
-    else
-      $user_to_save = Class_Users::newInstance()->setLogin($this->_identity);
-    
-    return  $user_to_save
-      ->beAbonneSIGB()
-      ->setLogin($this->_identity)
-      ->setPassword($this->_credential)
-      ->updateUser($user_from_sigb);
+    if (!$this->_bib)
+      return $new_user;
+
+    if ($user = Class_Users::findMatchingPatron($user_from_sigb, $this->_bib))
+      return $user;
+
+    return $new_user;
   }
 
 
@@ -89,6 +87,7 @@ class ZendAfi_Auth_Adapter_CommSigb implements Zend_Auth_Adapter_Interface {
     $bibs = $this->getBibsToAuthenticateTo($user);
 
     foreach($bibs as $bib) {
+      $this->_bib = $bib;
       if (!$emprunteur = $bib->getSIGBComm()->getEmprunteur($user))
         continue;
 
@@ -101,6 +100,7 @@ class ZendAfi_Auth_Adapter_CommSigb implements Zend_Auth_Adapter_Interface {
       $emprunteur->updateUser($user);
       return $user->setIdSite($bib->getId());
     }
+    $this->_bib = null;
     return  null;
   }
 
@@ -115,16 +115,16 @@ class ZendAfi_Auth_Adapter_CommSigb implements Zend_Auth_Adapter_Interface {
     if (!$user_in_db->isAbonne() || (!$bib = $user_in_db->getIntBib()) || (!$bib->getSIGBComm()))
       return [];
 
-    return [$bib]; 
+    return [$bib];
   }
 
 
   public function authResult($user) {
     $result = new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $this->_identity);
-    
+
     if(!$user)
       return new Zend_Auth_Result(Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identity);
-      
+
     if(!$user->save())
       $result = new Zend_Auth_Result(Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identity);
 
diff --git a/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php b/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php
index 4f53a1003ecd02502d18c7596498ba02debe3b60..5399f66fb9db6d94af2cb35f73b3672abc3b3870 100644
--- a/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php
+++ b/library/ZendAfi/Controller/Action/Helper/ViewRenderer.php
@@ -83,7 +83,7 @@ class ZendAfi_Controller_Action_Helper_ViewRenderer extends Zend_Controller_Acti
       return $this;
     }
 
-    (isTelephone() or $this->isEmbedded())
+    ((new Class_UserAgent())->isMobile() or $this->isEmbedded())
       ? $this->setLayoutScript("main.phtml")
       : $this->setLayoutScript("iphone.phtml");
 
diff --git a/library/ZendAfi/Controller/Plugin/DefineURLs.php b/library/ZendAfi/Controller/Plugin/DefineURLs.php
index d2872958adddbd35aa57b72aad759fddf136c217..fc7433fd483041912762479ef0e9372fe428ef35 100644
--- a/library/ZendAfi/Controller/Plugin/DefineURLs.php
+++ b/library/ZendAfi/Controller/Plugin/DefineURLs.php
@@ -41,7 +41,9 @@ class ZendAfi_Controller_Plugin_DefineURLs extends Zend_Controller_Plugin_Abstra
 
 
   public function shouldSelectTelephone($request) {
-    return ($request->getModuleName()=='telephone') || (isTelephone() and ('admin' !== $request->getModuleName()));
+    return
+      ($request->getModuleName()=='telephone')
+      || ((new Class_UserAgent())->isMobile() and ('admin' !== $request->getModuleName()));
   }
 
 
diff --git a/library/ZendAfi/View/Helper/GetSendProgressJsonFor.php b/library/ZendAfi/View/Helper/GetSendProgressJsonFor.php
index a245e69de1325b50929aad015ed62eaefe111165..c65a3d540cbeebbe080ef3e285701f49211e00a5 100644
--- a/library/ZendAfi/View/Helper/GetSendProgressJsonFor.php
+++ b/library/ZendAfi/View/Helper/GetSendProgressJsonFor.php
@@ -21,29 +21,43 @@
 
 class ZendAfi_View_Helper_getSendProgressJsonFor extends ZendAfi_View_Helper_BaseHelper {
   public function getSendProgressJsonFor($newsletter) {
-    if(!$newsletter)
-      return $this->toJson($this->_('Newsletter inconnue'));
+    if (!$newsletter)
+      return $this->_toStatus($this->_('Newsletter inconnue'));
 
+    ($last_date = DateTime::createFromFormat('Y-m-d H:i:s', $newsletter->getLastDistributionDate()))
+      ? $last_date_text = $this->_toStatus($last_date->format('d/m/Y H:i'))
+      : $last_date_text = $this->_toStatus($this->_('Aucune'));
 
-    ($last_date = DateTime::createFromFormat("Y-m-d H:i:s", $newsletter->getLastDistributionDate()))
-      ? $last_date = $this->toJson($last_date->format("d/m/Y H:i"))
-      : $last_date = $this->toJson($this->_('Aucune'));
+    if (!$dispatch = $newsletter->getLastDispatch())
+      return $last_date_text;
 
-    $dispatch = $newsletter->getLastDispatchInProgress();
-    if(!$dispatch)
-      return $last_date;
+    if (!$dispatch->hasEndedOn())
+      return array_merge($this->_toStatus(!$dispatch->getCollected()
+                                          ? $this->_('collecte des destinataires')
+                                          : $this->_('envoi en cours')),
+                         ['done' => $dispatch->numberOfDoneUsers(),
+                          'total' => $dispatch->numberOfDispatchUsers()]);
 
-    if (!$dispatch->getCollected())
-      return $this->toJson($this->_('en cours'));
-
-    return ['status' => $this->_('en cours'),
-            'done' => $dispatch->numberOfDoneUsers(),
-            'total' => $dispatch->numberOfDispatchUsers()];
+    return $dispatch->hasError()
+      ? $this->_toStatus($this->_tag('a', $this->_('Erreur lors de l\'envoi'),
+                                     ['data-popup' => 'true',
+                                      'href' => $this->view->url(['module' => 'admin',
+                                                                  'controller' => 'newsletter',
+                                                                  'action' => 'show-status',
+                                                                  'id' => $dispatch->getId()],
+                                                                 null, true)])
+                         . ($dispatch->getCollected()
+                            ? $this->_(', envoyé à %s sur %s',
+                                       $dispatch->numberOfDoneUsers(),
+                                       $dispatch->numberOfDispatchUsers())
+                            : $this->_(', aucun mail envoyé')))
 
+      : $this->_toStatus($last_date->format('d/m/Y H:i')
+                         . $this->_(', %s mails envoyés', $dispatch->numberOfDoneUsers()));
   }
 
 
-  protected function toJson($data) {
+  protected function _toStatus($data) {
     return ['status' => $data];
   }
 }
diff --git a/library/ZendAfi/View/Helper/ShareUrl.php b/library/ZendAfi/View/Helper/ShareUrl.php
index 82961559d44bcc2896f110d9fd629a8a09d40a5e..94a0439818d2633a866c4240da2a3bd407ce7225 100644
--- a/library/ZendAfi/View/Helper/ShareUrl.php
+++ b/library/ZendAfi/View/Helper/ShareUrl.php
@@ -21,15 +21,15 @@
 
 class ZendAfi_View_Helper_ShareUrl extends Zend_View_Helper_HtmlElement {
   static protected $_web_client;
+
   protected $reseaux=["facebook" => ["url" => "https://www.facebook.com/sharer/sharer.php"],
-                      "twitter"  => ["url" => "https://twitter.com/share?"]];
+                      "twitter"  => ["url" => "https://twitter.com/share"]];
 
 
   public function getReseaux($id_reseau=false)  {
-    if($id_reseau)
-      return $this->reseaux[$id_reseau];
-    else
-      return $this->reseaux;
+    return $id_reseau
+      ? $this->reseaux[$id_reseau]
+      : $this->reseaux;
   }
 
 
@@ -37,23 +37,25 @@ class ZendAfi_View_Helper_ShareUrl extends Zend_View_Helper_HtmlElement {
   public function shareUrl($id_reseau,$url_afi,$titre= '', $message = '', $url_img = '')  {
     $url_afi = $this->view->absoluteUrl(urldecode($url_afi));
 
-    // Url réseau
-    if($id_reseau==='facebook')
-      return $this->reseaux[$id_reseau]["url"];
-
-    if($id_reseau==='twitter')
-      return $this->reseaux[$id_reseau]["url"].
-        $this->getTwitterUrl($url_afi,
+    return $id_reseau === 'facebook'
+      ? $this->getFacebookUrl($url_afi, $titre)
+      : $this->getTwitterUrl($url_afi,
                              $titre,
                              $message);
   }
 
 
+  public function getFacebookUrl($url, $titre) {
+    return $this->reseaux['facebook']['url'] . '?' . http_build_query(['u' => $url,
+                                                                       'title' => $titre]);
+  }
+
+
   public function getTwitterUrl($url_afi, $titre, $message) {
-    return http_build_query(['url' => $url_afi,
-                             'text' => $titre,
-                             'counturl' => $url_afi],
-                            '','&');
+    return $this->reseaux['twitter']['url'] . '?' . http_build_query(['url' => $url_afi,
+                                                                      'text' => $titre,
+                                                                      'counturl' => $url_afi],
+                                                                     '','&');
   }
 
 
diff --git a/library/ZendAfi/View/Helper/TagModelTable.php b/library/ZendAfi/View/Helper/TagModelTable.php
index 37df6e4812d0cd359b604816f212f7ab5db3ef10..dff917e01e6fa956d419a1415749f05d122ea617 100644
--- a/library/ZendAfi/View/Helper/TagModelTable.php
+++ b/library/ZendAfi/View/Helper/TagModelTable.php
@@ -200,7 +200,7 @@ class ZendAfi_View_Helper_TagModelTable extends ZendAfi_View_Helper_BaseHelper {
 
     $content = $action['content'];
     $url = $this->view->url(['action' => $action['action'], 'id' => $model->getId()]);
-    $attribs = ['href' => $url];
+    $attribs = ['href' => $url, 'rel' => $action['action']];
     if (isset($_SERVER['REQUEST_URI'])
         && false !== strpos($_SERVER['REQUEST_URI'], $url))
       $attribs['class'] = 'selected';
diff --git a/library/ZendAfi/View/Helper/TagProgressBarForNewsletter.php b/library/ZendAfi/View/Helper/TagProgressBarForNewsletter.php
index edb05a1547c30f36aa19335b763fd79fcda07997..61aaf0fa303ba9b6c7ec14ebb9d432ef9b9232ae 100644
--- a/library/ZendAfi/View/Helper/TagProgressBarForNewsletter.php
+++ b/library/ZendAfi/View/Helper/TagProgressBarForNewsletter.php
@@ -25,20 +25,25 @@ class ZendAfi_View_Helper_TagProgressBarForNewsletter extends ZendAfi_View_Helpe
     $data_url = $this->view->url(['module'=>'admin',
                                   'controller' => 'newsletter',
                                   'action' => 'send-progress',
-                                  'id' => $newsletter->getId()],null,true);
+                                  'id' => $newsletter->getId()],
+                                 null, true);
+
     $progress_data = 'function getNewsletterProgress'.$newsletter->getId().'(){$.getJSON("'.$data_url.'", function (data) {'.
-      'if(!data.done || !data.total) {'.
-      '$("#progress_bar_newsletter_'.$newsletter->getId().'").text(data.status); return true;'.
+      '$("#progress_bar_newsletter_'.$newsletter->getId().' span").html(data.status);'.
+      'if(!data.total) {'.
+      '$("#progress_bar_newsletter_'.$newsletter->getId().'").html(data.status);'.
+      'initializePopups();'.
+      'return true;'.
       '}'.
       '$("#progress_bar_newsletter_'.$newsletter->getId().'").progressbar({value: data.done, max: data.total});'.
-      'setTimeout(function() {getNewsletterProgress'.$newsletter->getId().'()}, 100);'.
+      'setTimeout(function() {getNewsletterProgress'.$newsletter->getId().'();}, 100);'.
       '})};';
 
     Class_ScriptLoader::getInstance()
       ->addInlineScript($progress_data)
       ->addJQueryReady('getNewsletterProgress'.$newsletter->getId().'()');
 
-    return $this->_tag('div', '',
+    return $this->_tag('div', $this->_tag('span', ''),
                        ['id' => 'progress_bar_newsletter_' . $newsletter->getId()]);
   }
 }
\ No newline at end of file
diff --git a/library/fonctions/fonctions.php b/library/fonctions/fonctions.php
index 1cf786c3fc463294d8b13c3470220573b863467d..1a4dedc96117fa133225678683f3259203995b39 100644
--- a/library/fonctions/fonctions.php
+++ b/library/fonctions/fonctions.php
@@ -22,7 +22,6 @@ include_once( "string.php" );
 include_once( "error.php" );
 include_once( "sql.php" );
 include_once( "array.php" );
-include_once( "useragent.php" );
 include_once( "numbers.php" );
 include_once( 'cosmogramme/php/fonctions/date_heure.php');
 ?>
\ No newline at end of file
diff --git a/library/fonctions/useragent.php b/library/fonctions/useragent.php
deleted file mode 100644
index 689fe0673b25188b5f618ee175a52e30bc629e6d..0000000000000000000000000000000000000000
--- a/library/fonctions/useragent.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-/**
- * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
- *
- * BOKEH is free software; you can redistribute it and/or modify
- * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
- * the Free Software Foundation.
- *
- * There are special exceptions to the terms and conditions of the AGPL as it
- * is applied to this software (see README file).
- *
- * BOKEH 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 AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
- * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
- */
-function isTelephone() {
-  if (!array_key_exists('HTTP_USER_AGENT', $_SERVER))
-    return false;
-
-  // Test sur le user-agent
-  $regex_match="/(mobile|nokia|iphone|android|motorola|^mot\-|softbank|foma|docomo|kddi|up\.browser|up\.link|";
-  $regex_match.="htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|";
-  $regex_match.="blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam\-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|";
-  $regex_match.="symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte\-|longcos|pantech|gionee|^sie\-|portalmmm|";
-  $regex_match.="jig\s browser|hiptop|^ucweb|^benq|haier|^lct|opera\s*mobi|opera\*mini|320x320|240x320|176x220";
-  $regex_match.=")/i";
-
-  return (isset($_SERVER['HTTP_X_WAP_PROFILE']) or isset($_SERVER['HTTP_PROFILE']) or preg_match($regex_match, strtolower($_SERVER['HTTP_USER_AGENT'])));
-}
-
-
-function isUserAgentBot($useragent) {
-  return 0 !== preg_match('/(bot\/|sistrix|voilabot|slurp)/i', $useragent);
-}
-
-
-function isUserAgentBotAndNotAllowed() {
-  defineConstant('BOT_ALLOWED_START_HOUR', 20);
-  defineConstant('BOT_ALLOWED_END_HOUR', 8);
-
-  return isUserAgentBotAndNotAllowedBetweenHours(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
-                                                 BOT_ALLOWED_START_HOUR,
-                                                 BOT_ALLOWED_END_HOUR);
-}
-
-
-function isUserAgentBotAndNotAllowedBetweenHours($agent, $start_hour, $end_hour) {
-  if (!isUserAgentBot($agent))
-    return false;
-
-  $now = time();
-  $start = mktime($start_hour, 0, 0);
-  $end = mktime($end_hour, 0, 0);
-
-  return ($now < $start) && ($now > $end);
-}
-
-?>
\ No newline at end of file
diff --git a/library/startup.php b/library/startup.php
index 652559e3a70fc82a7131725b15d0e5f66159f253..2a7bafd378b4299160d2e57a53d2d398b46d18cb 100644
--- a/library/startup.php
+++ b/library/startup.php
@@ -83,7 +83,7 @@ class Bokeh_Engine {
 
   function setupConstants() {
     defineConstant('BOKEH_MAJOR_VERSION','7.7');
-    defineConstant('BOKEH_RELEASE_NUMBER', BOKEH_MAJOR_VERSION . '.13');
+    defineConstant('BOKEH_RELEASE_NUMBER', BOKEH_MAJOR_VERSION . '.14');
 
     defineConstant('BOKEH_REMOTE_FILES', 'http://git.afi-sa.fr/afi/opacce/');
 
diff --git a/scripts/sendNewsletter.php b/scripts/sendNewsletter.php
index 467cf67a308039bc00a7bbdb43bb7a0889458ae5..a90e862f4877919ab885d9686dfe0d932592ecb0 100644
--- a/scripts/sendNewsletter.php
+++ b/scripts/sendNewsletter.php
@@ -22,22 +22,29 @@
 define('BASE_URL', $argv[3]);
 $_SERVER['SERVER_NAME'] = $argv[2];
 $_SERVER['HTTP_HOST'] = $argv[1];
+$dispatch_id = $argv[4];
 
-set_include_path('.'
-								 . PATH_SEPARATOR .realpath(dirname(__FILE__)). '/../library'
-								 . PATH_SEPARATOR .realpath(dirname(__FILE__)). '/../library/storm/zf/library'
-								 . PATH_SEPARATOR .realpath(dirname(__FILE__)). '/../library/storm/src'
-								 . PATH_SEPARATOR .realpath(dirname(__FILE__)).'/../library/Class'
-								 . PATH_SEPARATOR .realpath(dirname(__FILE__)).'/../library/ZendAfi'
-. PATH_SEPARATOR . realpath(dirname(__FILE__)).'/../application/modules'
-. PATH_SEPARATOR . realpath(dirname(__FILE__)).'/application/modules'
-	);
+require_once 'includes.php';
 
-include_once "local.php";
-include_once "fonctions/fonctions.php";
-require_once "Zend/Loader.php";
-require_once "startup.php";
+$bokeh = (new Bokeh_Engine())->warmUp();
+if ('test' != $dispatch_id
+    && (!$dispatch = Class_Newsletter_Dispatch::find($dispatch_id))) {
+  printf("Unkown dispatch %s\n", $dispatch_id);
+  exit(1);
+}
 
-setupOpac();
+$my_shutdown = function() use ($dispatch) {
+  if (!$error = error_get_last())
+    return;
 
-Class_Newsletter_Dispatch::find($argv[4])->sendBy(20);
\ No newline at end of file
+  if (E_ERROR === $error['type'])
+    $dispatch->setError(json_encode($error))
+             ->beEnded();
+};
+
+register_shutdown_function($my_shutdown);
+
+$bokeh->powerOn();
+
+if ($dispatch)
+  $dispatch->sendBy(20);
\ No newline at end of file
diff --git a/tests/application/modules/admin/controllers/NewsletterControllerTest.php b/tests/application/modules/admin/controllers/NewsletterControllerTest.php
index b0d4e91b162d1a5178ebcd1a446d56fa2a3b420d..44b5a7511761730bb0a84d0fe93fd707689504bb 100644
--- a/tests/application/modules/admin/controllers/NewsletterControllerTest.php
+++ b/tests/application/modules/admin/controllers/NewsletterControllerTest.php
@@ -160,7 +160,7 @@ class Admin_NewsletterControllerIndexActionTest extends Admin_NewsletterControll
 
 
   public function testSendNouveautesClassiqueLink() {
-    $this->assertXPath("//a[@href='/admin/newsletter/send/id/1']");
+    $this->assertXPath("//a[@href='/admin/newsletter/send/id/1'][@rel='send']", $this->_response->getBody());
   }
 
 
@@ -485,15 +485,15 @@ class Admin_NewsletterControllerSendInProgressActionTest extends Admin_Newslette
     parent::setUp();
 
     $this->_command = $this->mock()
-                           ->whenCalled('exec')
-                           ->answers(true);
+                           ->whenCalled('exec')->answers(true)
+                           ->whenCalled('getOutput')->answers(['999'])
+                           ->whenCalled('getReturnVar')->answers(0);
 
     Class_Batch_SendNewsletters::setCommand($this->_command);
     Class_Newsletter_Dispatch::setTimeSource(new TimeSourceForTest('2016-07-21 11:21:38'));
 
-    $dispatch=Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(2));
+    $dispatch = Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(2));
     $dispatch->assertSave();
-    $dispatch->collectRecipients();
 
     $this->dispatch('/admin/newsletter/send/id/2', true);
     $this->_dispatch = Class_Newsletter::find(2)->getDispatchs()[0];
@@ -514,7 +514,108 @@ class Admin_NewsletterControllerSendInProgressActionTest extends Admin_Newslette
 
 
 
-class Admin_NewsletterControllerSendActionTest extends Admin_NewsletterControllerTestCase {
+class Admin_NewsletterControllerSendPreviouslyNotCollectedErrorActionTest
+  extends Admin_NewsletterControllerTestCase {
+
+  protected
+    $_command,
+    $_dispatch;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_command = $this->mock()
+                           ->whenCalled('exec')->answers(true)
+                           ->whenCalled('getOutput')->answers(['999'])
+                           ->whenCalled('getReturnVar')->answers(0);
+
+    Class_Batch_SendNewsletters::setCommand($this->_command);
+    Class_Newsletter_Dispatch::setTimeSource(new TimeSourceForTest('2016-07-21 11:21:38'));
+
+    Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(2))
+      ->setError(json_encode(['message' => 'An error occurred']))
+      ->setEndedOn('2012-10-26 12:03:45')
+      ->assertSave();
+
+    $this->dispatch('/admin/newsletter/send/id/2', true);
+  }
+
+
+  public function tearDown() {
+    Class_Batch_SendNewsletters::setCommand(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function shouldHaveCreateAnotherDispatch() {
+    $this->assertEquals(2, Class_Newsletter::find(2)->numberOfDispatchs());
+  }
+
+
+  /** @test */
+  public function shouldHaveADispatchInProgress() {
+    $this->assertNotNull(Class_Newsletter::find(2)->getLastDispatchInProgress());
+  }
+}
+
+
+
+class Admin_NewsletterControllerSendPreviouslyCollectedErrorActionTest
+  extends Admin_NewsletterControllerTestCase {
+
+  protected
+    $_command,
+    $_dispatch;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_command = $this->mock()
+                           ->whenCalled('exec')->answers(true)
+                           ->whenCalled('getOutput')->answers(['999'])
+                           ->whenCalled('getReturnVar')->answers(0);
+
+    Class_Batch_SendNewsletters::setCommand($this->_command);
+    Class_Newsletter_Dispatch::setTimeSource(new TimeSourceForTest('2016-07-21 11:21:38'));
+
+    Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(2))
+      ->setCollected(true)
+      ->setDispatchUsers([$this->fixture('Class_Newsletter_DispatchUser',
+                                         ['id' => '15', 'sent' => 1]),
+                          $this->fixture('Class_Newsletter_DispatchUser',
+                                         ['id' => '16', 'sent' => 0])])
+      ->setError(json_encode(['message' => 'An error occurred']))
+      ->setEndedOn('201-10-26 12:03:45')
+      ->assertSave();
+
+    $this->dispatch('/admin/newsletter/send/id/2', true);
+  }
+
+
+  public function tearDown() {
+    Class_Batch_SendNewsletters::setCommand(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function shouldHaveOnlyOneDispatch() {
+    $this->assertEquals(1, Class_Newsletter::find(2)->numberOfDispatchs());
+  }
+
+
+  /** @test */
+  public function shouldHaveADispatchInProgress() {
+    $this->assertNotNull(Class_Newsletter::find(2)->getLastDispatchInProgress());
+  }
+}
+
+
+
+class Admin_NewsletterControllerSendPreviouslyCollectedErrorButAllSentActionTest
+  extends Admin_NewsletterControllerTestCase {
+
   protected
     $_command,
     $_dispatch;
@@ -523,8 +624,61 @@ class Admin_NewsletterControllerSendActionTest extends Admin_NewsletterControlle
     parent::setUp();
 
     $this->_command = $this->mock()
-                           ->whenCalled('exec')
-                           ->answers(true);
+                           ->whenCalled('exec')->answers(true)
+                           ->whenCalled('getOutput')->answers(['999'])
+                           ->whenCalled('getReturnVar')->answers(0);
+
+    Class_Batch_SendNewsletters::setCommand($this->_command);
+    Class_Newsletter_Dispatch::setTimeSource(new TimeSourceForTest('2016-07-21 11:21:38'));
+
+    Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(2))
+      ->setCollected(true)
+      ->setDispatchUsers([$this->fixture('Class_Newsletter_DispatchUser',
+                                         ['id' => '15', 'sent' => 1]),
+                          $this->fixture('Class_Newsletter_DispatchUser',
+                                         ['id' => '16', 'sent' => 1])])
+      ->setError(json_encode(['message' => 'An error occurred']))
+      ->setEndedOn('201-10-26 12:03:45')
+      ->assertSave();
+
+    $this->dispatch('/admin/newsletter/send/id/2', true);
+  }
+
+
+  public function tearDown() {
+    Class_Batch_SendNewsletters::setCommand(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function shouldHaveOnlyOneDispatch() {
+    $this->assertEquals(2, Class_Newsletter::find(2)->numberOfDispatchs());
+  }
+
+
+  /** @test */
+  public function shouldHaveADispatchInProgress() {
+    $this->assertNotNull(Class_Newsletter::find(2)->getLastDispatchInProgress());
+  }
+}
+
+
+
+class Admin_NewsletterControllerSendActionTest
+  extends Admin_NewsletterControllerTestCase {
+
+  protected
+    $_command,
+    $_dispatch;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_command = $this->mock()
+                           ->whenCalled('exec')->answers(true)
+                           ->whenCalled('getOutput')->answers(['999'])
+                           ->whenCalled('getReturnVar')->answers(0);
 
     Class_Batch_SendNewsletters::setCommand($this->_command);
     Class_Newsletter_Dispatch::setTimeSource(new TimeSourceForTest('2016-07-21 11:21:38'));
@@ -555,7 +709,14 @@ class Admin_NewsletterControllerSendActionTest extends Admin_NewsletterControlle
   /** @test */
   public function commandShouldHaveBeenCalledWithDispatchId() {
     $this->assertContains(' "' . $this->_dispatch->getId() . '" ',
-                          $this->_command->getFirstAttributeForLastCallOn('exec'));
+                          $this->_command->getFirstAttributeForMethodCallAt('exec', 0));
+  }
+
+
+  /** @test */
+  public function commandShouldHaveBeenCalledWithBackgroudProcessId() {
+    $this->assertEquals('ps 999',
+                        $this->_command->getFirstAttributeForMethodCallAt('exec', 1));
   }
 
 
@@ -589,6 +750,54 @@ Lien pour se désinscrire de cette lettre d\'information : '. ROOT_URL . BASE_UR
 
 
 
+class Admin_NewsletterControllerSendActionWithCommandFailureTest extends Admin_NewsletterControllerTestCase {
+  protected
+    $_command,
+    $_dispatch;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_command = $this->mock()
+                           ->whenCalled('exec')->answers(true)
+                           ->whenCalled('getOutput')->answers(['999'])
+                           ->whenCalled('getReturnVar')->answers(1);
+
+    Class_Batch_SendNewsletters::setCommand($this->_command);
+    Class_Newsletter_Dispatch::setTimeSource(new TimeSourceForTest('2016-07-21 11:21:38'));
+
+    $this->dispatch('/admin/newsletter/send/id/2', true);
+    $this->_dispatch = Class_Newsletter::find(2)->getDispatchs()[0];
+  }
+
+
+  public function tearDown() {
+    Class_Batch_SendNewsletters::setCommand(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function shouldNotifyCommandError() {
+    $this->assertFlashMessengerContentContains('Envoi impossible : erreur à la création de la commande d\'envoi');
+  }
+
+
+  /** @test */
+  public function dispatchShouldHaveError() {
+    $this->assertContains('Unable to run /usr/bin/php -f',
+                          $this->_dispatch->getErrorMessage());
+  }
+
+
+  /** @test */
+  public function dispatchShouldBeEnded() {
+    $this->assertTrue($this->_dispatch->hasEndedOn());
+  }
+}
+
+
+
 
 class Admin_NewsletterControllerPreviewActionTest extends Admin_NewsletterControllerTestCase {
   public function setUp() {
@@ -1252,10 +1461,7 @@ class Admin_NewsletterControllerScriptTest extends Admin_NewsletterControllerTes
 
 abstract class Admin_NewsletterControllerSendProgressTestCase extends Admin_NewsletterControllerTestCase {
 
-  protected
-    $_storm_default_to_volatile = true,
-    $_json;
-
+  protected $_json;
 
   public function setUp() {
     parent::setUp();
@@ -1278,7 +1484,7 @@ class Admin_NewsletterControllerSendProgressWithWrongNewsletterIdTest extends Ad
 
 
   /** @test */
-  public function progressShouldReturnJsonUnknownNewsletter() {
+  public function statusShouldBeUnknownNewsletter() {
     $this->assertEquals('Newsletter inconnue', $this->_json->status);
   }
 
@@ -1290,56 +1496,99 @@ class Admin_NewsletterControllerSendProgressWithWrongNewsletterIdTest extends Ad
 
 
 
-class Admin_NewsletterControllerSendProgressEndTest extends Admin_NewsletterControllerSendProgressTestCase {
+class Admin_NewsletterControllerSendProgressWithErrorAndNothingSentTest
+  extends Admin_NewsletterControllerSendProgressTestCase {
+
+  protected function _dispatch() {
+    Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(1))
+      ->setError(json_encode(['message' => 'an error occured']))
+      ->setEndedOn('2016-10-27')
+      ->assertSave();
+
+    parent::_dispatch();
+  }
+
+
   /** @test */
-  public function progressShouldReturnEnCours() {
-    $this->assertEquals('en cours', $this->_json->status);
+  public function statusShouldContainsError() {
+    $this->assertContains('Erreur lors de l\'envoi', $this->_json->status);
   }
 
 
+  /** @test */
+  public function statusShouldContainsNothingSent() {
+    $this->assertContains('aucun mail envoyé', $this->_json->status);
+  }
+}
+
+
+
+class Admin_NewsletterControllerSendProgressWithErrorAnd1Of2SentTest
+  extends Admin_NewsletterControllerSendProgressTestCase {
+
   protected function _dispatch() {
-    Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(1))->assertSave();
+    $albator = $this->fixture('Class_Newsletter_DispatchUser',
+                              ['id' => '15', 'sent' => 1]);
+    $nausicaa = $this->fixture('Class_Newsletter_DispatchUser',
+                               ['id' => '16', 'sent' => 0]);
+
+    Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(1))
+      ->setCollected(true)
+      ->setDispatchUsers([$albator, $nausicaa])
+      ->setError(json_encode(['message' => 'an error occured']))
+      ->setEndedOn('2016-10-27')
+      ->assertSave();
+
     parent::_dispatch();
   }
-}
 
 
+  /** @test */
+  public function statusShouldContainsError() {
+    $this->assertContains('Erreur lors de l\'envoi', $this->_json->status);
+  }
+
 
-class Admin_NewsletterControllerSendProgressTest extends Admin_NewsletterControllerSendProgressTestCase {
   /** @test */
-  public function progressShouldReturnSending() {
-    $this->assertEquals('{"status":"en cours","done":0,"total":3}', $this->_response->getBody());
+  public function statusShouldContains1Of2Sent() {
+    $this->assertContains('envoyé à 1 sur 2', $this->_json->status);
   }
+}
 
 
-  protected function _dispatch() {
-    $dispatch = Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(1));
-    $dispatch->assertSave();
-    $dispatch->collectRecipients();
 
+class Admin_NewsletterControllerSendProgressNeverSentTest
+  extends Admin_NewsletterControllerSendProgressTestCase {
+
+  protected function _dispatch() {
+    Class_Newsletter::find(1)->setLastDistributionDate('')->assertSave();
     parent::_dispatch();
   }
+
+
+    /** @test */
+  public function progressShouldReturnNeverSent() {
+    $this->assertEquals('Aucune', $this->_json->status);
+  }
 }
 
 
 
-class Admin_NewsletterControllerNeverSendTest extends Admin_NewsletterControllerTestCase {
-  public function setUp() {
-    parent::setUp();
+class Admin_NewsletterControllerSendProgressWorkingTest
+  extends Admin_NewsletterControllerSendProgressTestCase {
 
-    $this->fixture('Class_Newsletter',
-                   ['id' => 3,
-                    'titre' => 'News of the Juny',
-                    'contenu' => 'A lot of new stuff',
-                    'last_distribution_date' => '']);
+  protected function _dispatch() {
+    $dispatch = Class_Newsletter_Dispatch::newFrom(Class_Newsletter::find(1));
+    $dispatch->assertSave();
+    $dispatch->collectRecipients();
 
-    $this->dispatch('admin/newsletter/send-progress/id/3', true);
+    parent::_dispatch();
   }
 
 
   /** @test */
-  public function progressShouldReturnNeverSendSending() {
-    $this->assertEquals('{"status":"Aucune"}', $this->_response->getBody());
+  public function progressShouldReturnSending() {
+    $this->assertEquals('{"status":"envoi en cours","done":0,"total":3}', $this->_response->getBody());
   }
 }
 
@@ -1549,3 +1798,34 @@ class Admin_NewsletterControllerEditSubcsriberReSubscriptionTest
   }
 
 }
+
+
+
+class Admin_NewsletterControllerShowStatusActionTest
+  extends Admin_NewsletterControllerTestCase {
+
+  /** @test */
+  public function withErrorShouldDisplayMessage() {
+    $this->fixture('Class_Newsletter_Dispatch', ['id' => 14])
+         ->setCollected(true)
+         ->setError(json_encode(['message' => 'An error occurred']))
+         ->setEndedOn('201-10-26 12:03:45')
+         ->assertSave();
+
+    $this->dispatch('/admin/newsletter/show-status/id/14', true);
+    $this->assertXPathContentContains('//p', 'An error occurred',
+                                      $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function withoutDispatchShouldBe404Error() {
+    try {
+      $this->dispatch('/admin/newsletter/show-status/id/14', true);
+    } catch(Zend_Controller_Action_Exception $e) {
+      $this->assertEquals(404, $e->getCode());
+      return;
+    }
+    $this->fail('should have 404 error');
+  }
+}
\ No newline at end of file
diff --git a/tests/application/modules/opac/controllers/AuthControllerTest.php b/tests/application/modules/opac/controllers/AuthControllerTest.php
index b6d91596f32ef7c646d0060c22e05de498379537..eb6590d40ae4e16d09ad508796a2ff90e1451106 100644
--- a/tests/application/modules/opac/controllers/AuthControllerTest.php
+++ b/tests/application/modules/opac/controllers/AuthControllerTest.php
@@ -2418,4 +2418,83 @@ class AuthControllerKohaPreRegistrationSuccessDispatchTest extends AuthControlle
   public function dateofBirthShouldBeDisbled() {
     $this->assertXPath('//form//input[@name="dateofbirth"][@type="text"][@disabled="disabled"][@value="15/09/1940"]');
   }
+}
+
+
+
+class AuthControllerPostWithSameIdSigbTest extends AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+
+  public function setUp() {
+    parent::setUp();
+
+    ZendAfi_Auth::getInstance()->clearIdentity();
+
+    $emprunteur = Class_WebService_SIGB_Emprunteur::newInstance(789, 'koha');
+    $emprunteur->setPassword('bar');
+    $emprunteur->beValid();
+
+    $service = $this->mock()
+                    ->whenCalled('getEmprunteur')
+                    ->answers($emprunteur)
+
+                    ->whenCalled('isConnected')
+                    ->answers(true);
+
+    $params = ['url_serveur' => 'http://mon-koha-de-test.org',
+               'id_bib' => 56,
+               'type' => Class_IntBib::COM_KOHA];
+
+    Class_WebService_SIGB_Koha::setService($params,
+                                           $service);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 56,
+                    'libelle' => 'Library']);
+
+    $this->fixture('Class_IntBib',
+                   ['id' => 56,
+                    'id_bib' => 56,
+                    'comm_sigb' => 5,
+                    'comm_params' => serialize($params)]);
+
+   $this->fixture('Class_Bib',
+                   ['id' => 13,
+                    'libelle' => 'First library']);
+
+    $this->fixture('Class_Users',
+                   ['id' => 123456,
+                    'login' => 'different',
+                    'password' => 'yes',
+                    'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                    'idabon' => 'different',
+                    'id_site' => 13,
+                    'id_sigb' => 789]);
+
+    $this->fixture('Class_Users',
+                   ['id' => 5,
+                    'login' => 'foo',
+                    'password' => 'bar',
+                    'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                    'idabon' => 'foo',
+                    'id_site' => 56,
+                    'id_sigb' => null]);
+
+    $this->postDispatch('/opac/auth/login',
+                        ['username' => 'foo',
+                         'password' => 'bar']);
+  }
+
+
+  /** @test */
+  public function userDifferentShouldNotBeUpdated() {
+    $this->assertEquals('different', Class_Users::find(123456)->getLogin());
+  }
+
+
+  /** @test */
+  public function userFooShouldBeLogged() {
+    $this->assertNotNull(Class_Users::getIdentity());
+    $this->assertEquals('foo', Class_Users::getIdentity()->getLogin());
+  }
 }
\ No newline at end of file
diff --git a/tests/application/modules/opac/controllers/BibControllerTest.php b/tests/application/modules/opac/controllers/BibControllerTest.php
index 283403563118a6711ad5622d90dfefdfae5f7149..29468ce1b1cca3dae6b0b0ab542b078c22786f1c 100644
--- a/tests/application/modules/opac/controllers/BibControllerTest.php
+++ b/tests/application/modules/opac/controllers/BibControllerTest.php
@@ -28,7 +28,6 @@ abstract class BibControllerWithZoneTestCase extends AbstractControllerTestCase
     unset($_REQUEST['geo_zone']);
     $_SESSION['selection_bib'] = ['nb_notices' => 12345, 'id_bibs' => null];
 
-
     $this->bib_annecy = $this->fixture('Class_Bib', ['id' => 4,
                                                      'id_zone' => 1,
                                                      'libelle' => 'Annecy',
@@ -711,7 +710,7 @@ class BibControllerBibViewAnnecyRangeOpeningsTest extends BibControllerBibViewTe
                    ['id' => 2,
                     'id_site' => 4,
                     'jour_semaine' => Class_Ouverture::LUNDI,
-                    'validity_start' => '2016-06-31',
+                    'validity_start' => '2016-06-30',
                     'validity_end' => '2016-08-31',
                     'label' => 'Horaires d\'été']);
 
@@ -719,7 +718,7 @@ class BibControllerBibViewAnnecyRangeOpeningsTest extends BibControllerBibViewTe
                    ['id' => 3,
                     'id_site' => 4,
                     'jour_semaine' => Class_Ouverture::MARDI,
-                    'validity_start' => '2016-06-31',
+                    'validity_start' => '2016-06-30',
                     'validity_end' => '2016-08-31',
                     'label' => 'I will not be displayed']);
 
@@ -727,7 +726,7 @@ class BibControllerBibViewAnnecyRangeOpeningsTest extends BibControllerBibViewTe
                    ['id' => 4,
                     'id_site' => 4,
                     'jour_semaine' => Class_Ouverture::MERCREDI,
-                    'validity_start' => '2016-06-31',
+                    'validity_start' => '2016-06-30',
                     'validity_end' => '2016-08-31',
                     'label' => 'Me neither']);
 
@@ -801,6 +800,62 @@ class BibControllerBibViewAnnecyRangeOpeningsTest extends BibControllerBibViewTe
 
 
 
+class BibControllerBibViewAnnecyWithOutdatedRangeOpeningsTest extends BibControllerBibViewTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    Class_Ouverture_Visitor::setTimeSource(new TimeSourceForTest('2016-10-25 16:08:42'));
+
+    $admin_bib = $this->fixture('Class_Users',
+                                ['id' => 54,
+                                 'login' => 'admin bib',
+                                 'password' => 'popup',
+                                 'id_site' => 4]);
+    $admin_bib->beAdminBib();
+    ZendAfi_Auth::getInstance()->logUser($admin_bib);
+
+    $this->fixture('Class_Ouverture',
+                   ['id' => 1,
+                    'id_site' => 4,
+                    'jour_semaine' => Class_Ouverture::LUNDI,
+                    'label' => 'D\'habitude']);
+
+    $this->fixture('Class_Ouverture',
+                   ['id' => 2,
+                    'id_site' => 4,
+                    'jour_semaine' => Class_Ouverture::LUNDI,
+                    'validity_start' => '2016-06-30',
+                    'validity_end' => '2016-08-31',
+                    'label' => 'Horaires d\'été']);
+
+
+    $this->dispatch('bib/bibview/id/4', true);
+  }
+
+
+  public function tearDown() {
+    Class_Ouverture_Visitor::setTimeSource(null);
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function shouldContainsDefault() {
+    $this->assertXPathContentContains('//div[contains(@class, "library_schedule")]//h3',
+                                      'D\'habitude');
+  }
+
+
+  /** @test */
+  public function shouldNotContainsHorairesDEte() {
+    $this->assertNotXPathContentContains('//div[contains(@class, "library_schedule")]//h3',
+                                         'Horaires d\'été',
+                                         $this->_response->getBody());
+  }
+}
+
+
+
 class BibControllerBibViewAnnecyFreeTextOpeningsTest extends BibControllerBibViewTestCase {
   public function setUp() {
     parent::setUp();
diff --git a/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php b/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php
index 633ec9009984130ffd46b9a316734cb729dcc9dc..33e751c02ca1ee748075f336500162c929b22e19 100644
--- a/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php
+++ b/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php
@@ -1630,6 +1630,33 @@ abstract class NoticeAjaxControllerCvsSearchTestCase extends AbstractControllerT
 
 
 
+
+class NoticeAjaxControllerCvsSearchFromGooglebotTest extends NoticeAjaxControllerCvsSearchTestCase {
+  protected $_backup_useragent;
+
+  public function setUp() {
+    parent::setUp();
+    $_SERVER['HTTP_USER_AGENT'] = 'Googlebot/2.1';
+  }
+
+
+
+  /** @test */
+  public function responseShouldBeEmpty() {
+    $this->dispatch('/opac/noticeajax/cvs-search/expressionRecherche/Cuisson', true);
+    $this->assertEmpty($this->_response->getBody());
+  }
+
+
+  public function tearDown() {
+    unset($_SERVER['HTTP_USER_AGENT']);
+    parent::tearDown();
+  }
+}
+
+
+
+
 class NoticeAjaxControllerCvsSearchWithNoRecordTest extends NoticeAjaxControllerCvsSearchTestCase {
 
   public function setUp() {
diff --git a/tests/application/modules/opac/controllers/ProfilOptionsControllerTest.php b/tests/application/modules/opac/controllers/ProfilOptionsControllerTest.php
index df6565f3398942b94a95fd2e113f43ef3960f618..653cddf117b9f621e9e076445f97360e3aa0a6a9 100644
--- a/tests/application/modules/opac/controllers/ProfilOptionsControllerTest.php
+++ b/tests/application/modules/opac/controllers/ProfilOptionsControllerTest.php
@@ -387,7 +387,10 @@ class ProfilOptionsControllerTwitterLinkWithProfilAdulteTest extends ProfilOptio
   /** @test */
   public function facebookLinkShouldReturnJavascriptForTweet() {
     $this->dispatch('/opac/index/share/on/facebook/titre/Profil+Adulte?url='.urlencode('/index/index'), true);
-    $this->assertContains("window.open('https://www.facebook.com/sharer/sharer.php','_blank','toolbar=0,status=0,width=800, height=410');",
+    $share_url = 'https://www.facebook.com/sharer/sharer.php?'
+      . http_build_query(['u' => Class_Url::absolute('/index/index?id_profil=22'),
+                          'title' => 'Profil Adulte']);
+    $this->assertContains("window.open('" . $share_url . "','_blank','toolbar=0,status=0,width=800, height=410');",
                           $this->_response->getBody());
   }
 }
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index 273b0d3d5b6d1f954efc32c78532e16abd552124..180c2838f5a89f680a7194a5998e790f54c16a0d 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -1134,3 +1134,19 @@ class UpgradeDB_312_Test extends UpgradeDBTestCase {
     $this->assertFieldType('ouvertures', 'multimedia', 'tinyint(1)');
   }
 }
+
+
+
+class UpgradeDB_313_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    try {
+      $this->query('alter table newsletter_dispatch drop column error');
+    } catch(Exception $e) {}
+  }
+
+
+  /** @test */
+  public function dispatchShouldHaveErrorColumn() {
+    $this->assertFieldType('newsletter_dispatch', 'error', 'longtext');
+  }
+}
diff --git a/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php b/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php
index 92a075150d03585615837981ba52388dde0d0ac8..ebf3ae25e7ae83e07b7415ffc9c1cbf4735ba4c1 100644
--- a/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php
+++ b/tests/library/Class/MoteurRecherche/MoteurRechercheFacettesTest.php
@@ -203,27 +203,27 @@ class MoteurRechercheFacettesTest extends ModelTestCase {
 
   /** @test */
   public function bookmarksShouldContainsDomainGamesFacet() {
-    $this->assertEquals(['id' => 'HCCCC0001',
-                         'label' => 'Domaines : Games (1)'],
-                        $this->facettes['bookmarks'][0]);
+    $this->assertContains(['id' => 'HCCCC0001',
+                           'label' => 'Domaines : Games (1)'],
+                          $this->facettes['bookmarks']);
   }
 
 
   /** @test */
   public function bookmarksShouldContainsLibCran() {
-    $this->assertEquals(['id' => 'B3',
-                         'label' => 'Bibliothèque : Cran (1)'],
-                        $this->facettes['bookmarks'][1]);
+    $this->assertContains(['id' => 'B3',
+                           'label' => 'Bibliothèque : Cran (1)'],
+                          $this->facettes['bookmarks']);
   }
 
 
   /** @test */
   public function bookmarksWithEmptySearchShouldNotBeEmpty() {
     $this->setupSearch('');
-    $this->assertEquals([['id' => 'HCCCC0001',
-                          'label' => 'Domaines : Games (1)'],
-                         ['id' => 'B3',
-                          'label' => 'Bibliothèque : Cran (1)']], $this->facettes['bookmarks']);
+    $this->assertEquals([['id' => 'B3',
+                          'label' => 'Bibliothèque : Cran (1)'],
+                         ['id' => 'HCCCC0001',
+                          'label' => 'Domaines : Games (1)']], $this->facettes['bookmarks']);
   }
 
 
diff --git a/tests/library/ZendAfi/Auth/Adapter/AuthCommSigbTest.php b/tests/library/ZendAfi/Auth/Adapter/AuthCommSigbTest.php
index c129489b944f0883fb8dc414bca5cf5e11643ff2..baa6540765bb3050dc5a72c29a8690c988588b86 100644
--- a/tests/library/ZendAfi/Auth/Adapter/AuthCommSigbTest.php
+++ b/tests/library/ZendAfi/Auth/Adapter/AuthCommSigbTest.php
@@ -16,7 +16,7 @@
  *
  * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
  * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
 abstract class AuthCommSigbTestCase extends Storm_Test_ModelTestCase {
@@ -25,7 +25,7 @@ abstract class AuthCommSigbTestCase extends Storm_Test_ModelTestCase {
 
     Class_Users::beVolatile();
     Class_IntBib::beVolatile();
-    
+
     $this->_zork = $this->fixture('Class_Users',
                                   ['id' => 4,
                                    'date_fin' => '2010-10-23',
@@ -34,7 +34,7 @@ abstract class AuthCommSigbTestCase extends Storm_Test_ModelTestCase {
                                    'idabon' => '98475',
                                    'id_site' => 2,
                                    'password' => 'xzy']);
-    
+
     Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Users');
     Storm_Test_ObjectWrapper::onLoaderOfModel('Class_IntBib');
   }
@@ -95,7 +95,7 @@ abstract class AuthCommSigbWithWebServicesAndAbonneZorkTestCase extends AuthComm
                 ->setEndDate('2015-12-25')
                 ->setPassword('secret')
                 ->beValid());
-    $this->_zork->setIdSite(74);
+    $this->_zork->setIdSite(74)->save();
 
     return $this;
   }
@@ -112,8 +112,8 @@ class AuthCommSigbSuccessfullAuthenticationTest extends AuthCommSigbWithWebServi
       ->whenCalled('save')
       ->willDo(
         function($user) {
-          $user->setId(23); 
-          return true; 
+          $user->setId(23);
+          return true;
         });
 
     $this->_adapter = (new ZendAfi_Auth_Adapter_CommSigb())
@@ -122,7 +122,7 @@ class AuthCommSigbSuccessfullAuthenticationTest extends AuthCommSigbWithWebServi
 
     $this->_authenticate_result = $this->_adapter->authenticate();
   }
-  
+
 
   /** @test */
   public function authenticateZorkShouldReturnValidResult() {
@@ -142,7 +142,7 @@ class AuthCommSigbSuccessfullAuthenticationTest extends AuthCommSigbWithWebServi
     $this->assertTrue($user->isAbonne());
   }
 
- 
+
   /** @test */
   public function resultObjectShouldBeSetUp() {
     $result = $this->_adapter->getResultObject();
@@ -169,14 +169,14 @@ class AuthCommSigbSuccessfullAuthenticationWithExistingUserTest extends AuthComm
 
     $this->_authenticate_result = $this->_adapter->authenticate();
   }
-  
+
 
   /** @test */
   public function authenticateZorkShouldReturnValidResult() {
     $this->assertTrue($this->_authenticate_result->isValid());
   }
 
-  
+
   /** @test */
   public function zorkShouldHaveBeenSaved() {
     $this->assertEquals($this->_zork, Class_Users::getFirstAttributeForLastCallOn('save'));
@@ -192,13 +192,13 @@ class AuthCommSigbSuccessfullAuthenticationWithExistingUserTest extends AuthComm
 
   /** @test */
   public function zorkDateFinShouldBeUpdatedTo2015_12_25() {
-    $this->assertEquals('2015-12-25', $this->_zork->getDateFin());    
+    $this->assertEquals('2015-12-25', $this->_zork->getDateFin());
   }
 
 
   /** @test */
   public function zorkPasswordShouldBeSecret() {
-    $this->assertEquals('secret', $this->_zork->getPassword());   
+    $this->assertEquals('secret', $this->_zork->getPassword());
   }
 }
 
@@ -213,7 +213,7 @@ class AuthCommSigbSuccessfullAuthenticationWithExistingUserButWrongPasswordTest
       ::whenCalled('findAllBy')
       ->with(['login' => 'zork_sigb'])
       ->answers([$this->_zork]);
-    
+
     $this->_adapter = (new ZendAfi_Auth_Adapter_CommSigb())
       ->setIdentity('zork_sigb')
       ->setCredential('oups');
@@ -234,7 +234,7 @@ class AuthCommSigbSuccessfullAuthenticationWithExistingUserButWrongPasswordTest
 class AuthCommSigbErrorsTest extends AuthCommSigbWithWebServicesAndAbonneZorkTestCase {
   public function setUp() {
     parent::setUp();
-    
+
     $this->_adapter = (new ZendAfi_Auth_Adapter_CommSigb())
                          ->setIdentity('zork_sigb')
                          ->setCredential('blabla');
@@ -265,7 +265,7 @@ class AuthCommSigbErrorsTest extends AuthCommSigbWithWebServicesAndAbonneZorkTes
 class AuthCommSigbSuccessfullAuthenticationWithExistingFamilleUserTest extends AuthCommSigbWithWebServicesAndAbonneZorkTestCase {
 
   protected $_zowife, $_zork_boy, $_zork_girl;
-    
+
 
   public function setUp() {
     parent::setUp();
@@ -274,19 +274,20 @@ class AuthCommSigbSuccessfullAuthenticationWithExistingFamilleUserTest extends A
 
     $this->_zowife = $this->fixture('Class_Users',
                                     ['id' => 45,
-                                     'password' => 'zowife', 
+                                     'password' => 'zowife',
                                      'login' => 'zork_sigb']);
 
     $this->_zork_boy = $this->fixture('Class_Users',
                                       ['id' => 65,
-                                       'password' => 'zorkboy', 
+                                       'password' => 'zorkboy',
                                        'login' => 'zork_sigb']);
-    
+
     $this->_zork_girl = $this->fixture('Class_Users',
                                        ['id' => 85,
-                                        'password' => 'zorkgirl', 
+                                        'password' => 'zorkgirl',
                                         'login' => 'zork_sigb',
-                                        'id_sigb' => '001234']);
+                                        'id_sigb' => '001234',
+                                        'id_site' => 74]);
 
     $this->opsys
       ->whenCalled('getEmprunteur')
@@ -297,15 +298,15 @@ class AuthCommSigbSuccessfullAuthenticationWithExistingFamilleUserTest extends A
                 ->setPassword('zorkgirl')
                 ->beValid());
 
-    
+
     $this->_adapter = (new ZendAfi_Auth_Adapter_CommSigb())
       ->setIdentity('zork_sigb')
       ->setCredential('zorkgirl');
 
     $this->_authenticate_result = $this->_adapter->authenticate();
   }
-  
-  
+
+
   /** @test **/
   public function zorkGirlShouldHaveBeenSave() {
     $this->assertTrue(Class_Users::methodHasBeenCalledWithParams('save', [$this->_zork_girl]));
@@ -336,15 +337,15 @@ class AuthCommSigbSuccessfullAuthenticationWithNewLoginTest extends AuthCommSigb
                 ->setPassword('xzyz')
                 ->beValid());
 
-    
+
     $this->_adapter = (new ZendAfi_Auth_Adapter_CommSigb())
       ->setIdentity('new_login')
       ->setCredential('xzyz');
 
     $this->_authenticate_result = $this->_adapter->authenticate();
   }
-  
-  
+
+
   /** @test */
   public function authenticateZorkShouldBeValid() {
     $this->assertTrue($this->_authenticate_result->isValid());
@@ -382,15 +383,15 @@ class AuthCommSigbErrorAuthenticationWithNewLoginTest extends AuthCommSigbWithWe
                 ->setCodeBarres('1234')
                 ->beValid());
 
-    
+
     $this->_adapter = (new ZendAfi_Auth_Adapter_CommSigb())
       ->setIdentity('new_login')
       ->setCredential('xzyz');
 
     $this->_authenticate_result = $this->_adapter->authenticate();
   }
-  
-  
+
+
   /** @test */
   public function authenticateZorkShouldBeValid() {
     $this->assertTrue($this->_authenticate_result->isValid());
@@ -431,14 +432,15 @@ class AuthCommSigbAuthenticationSetupInvalidUserTest extends AuthCommSigbWithWeb
   /** @test */
   public function authenticateZorkShouldNotBeValid() {
     $this->assertFalse($this->_authenticate_result->isValid());
-  } 
+  }
 
 
   /** @test */
-  public function noUserShouldHaveBeenSaved() {
-    $this->assertFalse(Class_Users::methodHasBeenCalled('save'));
+  public function noUserShouldHaveBeenAdded() {
+    $this->assertCount(1, Class_Users::findAll());
   }
 
+
   /** @test */
   public function zorkPasswordShouldRemainXZY() {
     $this->assertEquals('xzy', $this->_zork->getPassword());
diff --git a/tests/library/ZendAfi/View/Helper/TagProgressBarForNewsletterTest.php b/tests/library/ZendAfi/View/Helper/TagProgressBarForNewsletterTest.php
index a2d73cbfa62e164062ca2604604eb66bd2e74f1f..8c6fc27a687c7e7bca88bd23d4b41839a4050d41 100644
--- a/tests/library/ZendAfi/View/Helper/TagProgressBarForNewsletterTest.php
+++ b/tests/library/ZendAfi/View/Helper/TagProgressBarForNewsletterTest.php
@@ -53,7 +53,8 @@ class ZendAfi_View_Helper_TagProgressBarForNewsletterTest extends ViewHelperTest
 
   /** @test */
   public function shouldReturnDivProgressBar() {
-    $this->assertEquals('<div id="progress_bar_newsletter_1"></div>', $this->_helper->tagProgressBarForNewsletter($this->newsletter));
+    $this->assertXPath($this->_helper->tagProgressBarForNewsletter($this->newsletter),
+                       '//div[@id="progress_bar_newsletter_1"]');
   }
 
 
diff --git a/tests/library/fonctions/UserAgentTest.php b/tests/library/fonctions/UserAgentTest.php
index daa5808bf06f2f020e5cbdd8b8b106b85646659f..4c8c213fb3428b010d9f164178b767737fab4e60 100644
--- a/tests/library/fonctions/UserAgentTest.php
+++ b/tests/library/fonctions/UserAgentTest.php
@@ -16,31 +16,31 @@
  *
  * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
  * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
 class BotUserAgentTest extends PHPUnit_Framework_TestCase {
   /** @test */
   public function ifBotBetweenAllowedHoursShouldReturnFalse() {
-    $this->assertFalse(isUserAgentBotAndNotAllowedBetweenHours('googlebot/',
-                                                               date('H')-1,
-                                                               date('H')+2));
+    $this->assertFalse((new Class_UserAgent('googlebot/'))
+                       ->isBotAndNotAllowedBetweenHours(date('H')-1,
+                                                        date('H')+2));
   }
 
 
   /** @test */
   public function ifBotOutsideAllowedHoursShouldReturnTrue() {
-    $this->assertTrue(isUserAgentBotAndNotAllowedBetweenHours('googlebot/',
-                                                              date('H')+1,
-                                                              date('H')-2));
+    $this->assertTrue((new Class_UserAgent('googlebot/'))
+                       ->isBotAndNotAllowedBetweenHours(date('H')+1,
+                                                        date('H')-2));
   }
 
 
   /** @test */
   public function ifNotBotOutsideAllowedHoursShouldReturnFalse() {
-    $this->assertFalse(isUserAgentBotAndNotAllowedBetweenHours('firefox',
-                                                               date('H')+1,
-                                                               date('H')+2));
+    $this->assertFalse((new Class_UserAgent('firefox'))
+                       ->isBotAndNotAllowedBetweenHours(date('H')+1,
+                                                        date('H')+2));
   }
 
 
@@ -52,12 +52,12 @@ class BotUserAgentTest extends PHPUnit_Framework_TestCase {
       ];
   }
 
-  /** 
+  /**
    * @dataProvider botUserAgents
-   * @test 
+   * @test
    */
   public function isUserAgentShouldReturnTrueForAgent($agent) {
-    $this->assertTrue(isUserAgentBot($agent));
+    $this->assertTrue((new Class_UserAgent($agent))->isBot());
   }
 
 
@@ -69,12 +69,12 @@ class BotUserAgentTest extends PHPUnit_Framework_TestCase {
       ];
   }
 
-  /** 
+  /**
    * @dataProvider browserUserAgents
-   * @test 
+   * @test
    */
   public function isUserAgentShouldReturnFalseForAgent($agent) {
-    $this->assertFalse(isUserAgentBot($agent));
+    $this->assertFalse((new Class_UserAgent($agent))->isBot());
   }
 }
 
diff --git a/tests/scripts/SendNewsletterTest.php b/tests/scripts/SendNewsletterTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e7ec3e7f9a8358ad5a06f387cb124e08eba5ef7
--- /dev/null
+++ b/tests/scripts/SendNewsletterTest.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Scripts_SendNewsletterTest extends PHPUnit_Framework_TestCase {
+  /** @test */
+  public function shouldSucceed() {
+    exec('cd ' . __DIR__ . '/../.. && php -f scripts/sendNewsletter.php "localhost" "localhost" "" "test"',
+         $output, $result);
+
+    $this->assertEquals(0, $result, implode("\n", $output));
+  }
+}
\ No newline at end of file