diff --git a/FEATURES/107087 b/FEATURES/107087
new file mode 100644
index 0000000000000000000000000000000000000000..2bd738944292f94393ec692f1636f11b964e7625
--- /dev/null
+++ b/FEATURES/107087
@@ -0,0 +1,10 @@
+        '107087' =>
+            ['Label' => $this->_('Export des agendas au format OpenAgenda'),
+             'Desc' => $this->_('Exportez vos agendas pour les intégrer dans un logiciel de calendrier ou un autre système d\'information.'),
+             'Image' => '',
+             'Video' => '',
+             'Category' => $this->_('Agenda'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => '',
+             'Test' => '',
+             'Date' => '2020-08-13'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/107087 b/VERSIONS_WIP/107087
new file mode 100644
index 0000000000000000000000000000000000000000..8dc46bc814fb8f77b0133c64f4118a36b03b03c7
--- /dev/null
+++ b/VERSIONS_WIP/107087
@@ -0,0 +1 @@
+ - ticket #107087 : Export des agendas au format OpenAgenda
\ No newline at end of file
diff --git a/application/modules/opac/controllers/CmsController.php b/application/modules/opac/controllers/CmsController.php
index e743955ef5bd61e7eda00a8f4b94d2b868312141..27b212577a13b24bc16c4c3fc371c59ec757e9cf 100644
--- a/application/modules/opac/controllers/CmsController.php
+++ b/application/modules/opac/controllers/CmsController.php
@@ -183,6 +183,13 @@ class CmsController extends ZendAfi_Controller_Action {
   }
 
 
+  public function openagendaAction() {
+    $profil = Class_Profil::find((int)$this->_getParam('id_profil', 1));
+    $id_module = (int)$this->_getParam('id_module');
+    $this->_helper->renderOpenagenda($profil, $id_module);
+  }
+
+
   /**
    * @see ZendAfi_View_Helper_Accueil_MenuVertical
    */
diff --git a/library/Class/Article/Openagenda.php b/library/Class/Article/Openagenda.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5bc39efb9a4726c148e99f5378d6bb66022bdf3
--- /dev/null
+++ b/library/Class/Article/Openagenda.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, 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_Article_Openagenda {
+  protected
+    $_field_map = [
+                   'id_article' => 'uid',
+                   'titre' => 'title',
+                   'description' => 'description',
+                   'contenu' => 'html',
+                   'tags' => 'keywords',
+                   'date_maj' => 'updatedAt'
+    ],
+    $_location_field_map = [
+                            'id' => 'uid',
+                            'libelle' => 'name',
+                            'adresse' => 'address',
+                            'code_postal' => 'postalCode',
+                            'latitude' => 'latitude',
+                            'longitude' => 'longitude',
+                            'ville' => 'city',
+                            'pays' => 'country',
+                            'telephone' => 'phone',
+                            'url' => 'website',
+                            'mail' => 'access'
+    ],
+    $_datas,
+    $_article;
+
+
+  public function __construct($article) {
+    $this->_article = $article;
+  }
+
+
+  public function asOpenagenda() {
+    $attributes = $this->_article->getRawAttributes();
+    $this->_datas = $this->_applyMapping($this->_field_map, $attributes, true);
+    $this->_datas['updatedAt'] = date('c', strtotime($this->_datas['updatedAt']));
+
+    $this
+      ->_applyLocation()
+      ->_applyTimings()
+      ->_applyCustomFields();
+
+    return $this->_datas;
+  }
+
+
+  protected function _applyMapping($map, $datas, $empty_accepted = false) {
+    $mapped_datas = [];
+
+    $datas = array_intersect_key(array_change_key_case($datas, CASE_LOWER),
+                                 $map);
+
+    foreach($datas as $key => $value)
+      $mapped_datas[$map[$key]] = $value;
+
+    return $empty_accepted
+      ? $mapped_datas
+      : array_filter($mapped_datas);
+  }
+
+
+  protected function _applyCustomFields() {
+    if (!isset($this->_datas['keywords']))
+      $this->_datas['keywords'] = '';
+
+    $values = array_map(function($custom_value)
+                        {
+                          return implode(';', $custom_value->getValueAsArray());
+                        },
+                        Class_CustomField_Value::findAllByInstance($this->_article));
+
+    $this->_datas['keywords'] .= implode (';', array_filter($values));
+
+    return $this;
+  }
+
+
+  protected function _applyLocation() {
+    if (!$lieu = $this->_article->getLieu())
+      return $this;
+
+    $this->_datas['location'] = $this->_applyMapping($this->_location_field_map,
+                                                     $lieu->getRawAttributes());
+
+    return $this;
+  }
+
+
+  protected function _applyTimings() {
+    if (!($events_debut = $this->_article->getEventsDebut())
+        || !($events_fin = $this->_article->getEventsFin()))
+      return $this;
+
+    $debut = explode(' ', $events_debut);
+    $fin = explode(' ', $events_fin);
+
+    $this->_datas['firstDate'] = $debut[0];
+    $this->_datas['lastDate'] = $fin[0];
+
+    $this->_datas['firstTimeStart'] = substr($debut[1], 0, 5);
+    $this->_datas['lastTimeEnd'] = substr($fin[1], 0, 5);
+
+    $this->_datas['timings'] = ['start' => date('c', strtotime($events_debut)),
+                                'end' => date('c', strtotime($events_fin))];
+
+    return $this;
+ }
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/RenderOpenagenda.php b/library/ZendAfi/Controller/Action/Helper/RenderOpenagenda.php
new file mode 100644
index 0000000000000000000000000000000000000000..85a1921261b457f6ddfb0385040b01074e7817e7
--- /dev/null
+++ b/library/ZendAfi/Controller/Action/Helper/RenderOpenagenda.php
@@ -0,0 +1,40 @@
+<?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 ZendAfi_Controller_Action_Helper_RenderOpenagenda
+  extends Zend_Controller_Action_Helper_Abstract {
+
+  public function direct($profil, $module_id) {
+    $this->_actionController->getHelper('ViewRenderer')->setNoRender();
+    $this->_actionController->getResponse()->setHeader('Content-Type',
+                                                       'application/json;charset=utf-8');
+
+    if (!$profil
+        || (!$preferences = $profil->getModuleAccueilPreferences($module_id, 'CALENDAR')))
+      return $this->_actionController->getResponse()->setBody('[]');
+
+    $articles_json = [];
+    foreach (Class_Article::getArticlesByPreferences($preferences) as $article)
+      $articles_json []= (new Class_Article_Openagenda($article))->asOpenagenda();
+
+    $this->_actionController->getResponse()
+                            ->setBody(json_encode($articles_json));
+  }
+}
diff --git a/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php b/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php
index 6ff1eae9a5d754501a70ddf62e45d662b2e620f7..36ea3eea15bb7b4224cf1aa660172b2a7bb88361 100644
--- a/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php
+++ b/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php
@@ -748,11 +748,10 @@ class CmsControllerCalendarActionAjaxLinkTest extends AbstractControllerTestCase
 
 
 
-abstract class CmsControllerCalendarActionIcalExportTestCase
+abstract class CmsControllerCalendarActionFixturesTestCase
   extends CmsControllerCalendarActionTestCase {
 
   protected
-    $_ical = '',
     $_articles_by_pref = [];
 
   public function setUp() {
@@ -790,7 +789,6 @@ abstract class CmsControllerCalendarActionIcalExportTestCase
       ->answers($this->_articles_by_pref);
 
     $this->_dispatch();
-    $this->_ical = $this->_response->getBody();
   }
 
 
@@ -800,7 +798,19 @@ abstract class CmsControllerCalendarActionIcalExportTestCase
   protected function _dispatch() {
     $this->dispatch('/cms/ical');
   }
+}
+
+
+abstract class CmsControllerCalendarActionIcalExportTestCase
+  extends CmsControllerCalendarActionFixturesTestCase {
+
+  protected
+    $_ical = '';
 
+  public function setUp(){
+    parent::setUp();
+    $this->_ical = $this->_response->getBody();
+  }
 
   /** @test */
   public function contentTypeHeaderShouldBeTextCalendarUtf8() {
@@ -954,6 +964,164 @@ class CmsControllerCalendarActionIcalExportSimpleTest
 
 
 
+
+abstract class AbstractCmsControllerCalendarActionOpenagendaExportTestCase
+  extends CmsControllerCalendarActionFixturesTestCase {
+  protected $_json_content = [];
+
+  public function setUp() {
+    parent::setUp();
+    $this->_json_content = json_decode($this->_response->getBody(), true);
+  }
+
+
+  protected function _dispatch(){
+    $this->dispatch('cms/openagenda');
+  }
+}
+
+
+
+
+class CmsControllerCalendarActionOpenagendaExportEmptyDataTest
+  extends AbstractCmsControllerCalendarActionOpenagendaExportTestCase {
+
+
+  /** @test */
+  public function headerContentTypeShouldContainsApplicationJson() {
+    $this->assertHeaderContains('Content-type', 'application/json');
+  }
+
+
+  /** @test */
+  public function withEmptyPrefsJsonContentShouldBeEmpty() {
+    $this->assertEmpty($this->_json_content);
+  }
+
+}
+
+
+class CmsControllerCalendarActionOpenagendaExportSimpleTest
+  extends AbstractCmsControllerCalendarActionOpenagendaExportTestCase {
+
+
+  protected function _prepareFixtures() {
+    $this->setupCustomFields();
+    $this->_nanook2
+      ->setCustomField('Theme', ['sigb'])
+      ->setCustomField('Options "classieuses"', ['wifi','restauration'])
+      ->saveWithCustomFields();
+
+    $this->_nanook2
+      ->setLieu($this->annecy)
+      ->setTags('Jeunesse;Interface')
+      ->setDescription('Une super description')
+      ->setDateMaj('2018-06-12 12:47:36')
+      ->assertSave();
+
+    $this->_articles_by_pref = [$this->_nanook2];
+  }
+
+
+  /** @test */
+  public function headerContentTypeShouldContainsApplicationJson() {
+    $this->assertHeaderContains('Content-type', 'application/json');
+  }
+
+
+  /** @test */
+  public function uidShouldBe4() {
+    $this->assertEquals(4, $this->_json_content[0]['uid']);
+  }
+
+
+  /** @test */
+  public function titleShouldContainsNanook2EnProd() {
+    $this->assertContains('Nanook 2 en prod', $this->_json_content[0]['title']);
+  }
+
+
+  /** @test */
+  public function htmlShouldContainsYoupi() {
+    $this->assertContains('youpi !', $this->_json_content[0]['html']);
+  }
+
+
+  /** @test */
+  public function descriptionShouldContainsSuperDescription() {
+    $this->assertContains('super description', $this->_json_content[0]['description']);
+  }
+
+
+  /** @test */
+  public function updatedAtShouldBeFoo2018_06_12() {
+    $this->assertEquals('2018-06-12T12:47:36+02:00',
+                        $this->_json_content[0]['updatedAt']);
+  }
+
+
+  /** @test */
+  public function firstDateShouldContains20110217() {
+    $this->assertEquals('2011-02-17', $this->_json_content[0]['firstDate']);
+  }
+
+
+  /** @test */
+  public function firstTimeStartShouldBeMidnight() {
+    $this->assertEquals('00:00', $this->_json_content[0]['firstTimeStart']);
+  }
+
+
+  /** @test */
+  public function lastDateShouldContains20110222() {
+    $this->assertEquals('2011-02-22', $this->_json_content[0]['lastDate']);
+  }
+
+
+  /** @test */
+  public function lastTimeEndShouldBeMidnight() {
+    $this->assertEquals('00:00', $this->_json_content[0]['lastTimeEnd']);
+  }
+
+
+  /** @test */
+  public function timingsShouldContains20110222() {
+    $this->assertEquals(['start' => '2011-02-17T00:00:00+01:00',
+                         'end'   => '2011-02-22T00:00:00+01:00'],
+                        $this->_json_content[0]['timings']);
+  }
+
+
+  /** @test */
+  public function locationShouldBeAnnecy() {
+    $annecy_place = ['uid' => '41',
+                     'name' => 'Annecy',
+                     'address' => 'Rue des tomates',
+                     'postalCode' => '74000',
+                     'latitude' => '47.0999715',
+                     'longitude' => '5.4883906',
+                     'country' => 'FRANCE'];
+
+    $this->assertEquals($annecy_place,
+                        $this->_json_content[0]['location']);
+  }
+
+
+  /** @test */
+  public function keywordsShouldContainsSigb() {
+    $this->assertContains('sigb', $this->_json_content[0]['keywords']);
+  }
+
+
+  /** @test */
+  public function keywordsShouldContainsJeunesse() {
+    $this->assertContains('Jeunesse', $this->_json_content[0]['keywords']);
+  }
+}
+
+
+
+
 class CmsControllerCalendarActionIcalExportSimpleRecurrentTest
   extends CmsControllerCalendarActionIcalExportTestCase {