diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2067,6 +2067,7 @@
     'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
     'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php',
     'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php',
+    'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php',
     'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
     'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
     'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
@@ -6915,6 +6916,7 @@
     'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField',
     'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType',
+    'PhabricatorCalendarEventNotificationView' => 'Phobject',
     'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
diff --git a/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php b/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php
--- a/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php
+++ b/src/applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php
@@ -10,13 +10,28 @@
       ->setSynopsis(
         pht(
           'Test and debug notifications about upcoming events.'))
-      ->setArguments(array());
+      ->setArguments(
+        array(
+          array(
+            'name' => 'minutes',
+            'param' => 'N',
+            'help' => pht(
+              'Notify about events in the next __N__ minutes (default: 15). '.
+              'Setting this to a larger value makes testing easier.'),
+          ),
+        ));
   }
 
   public function execute(PhutilArgumentParser $args) {
     $viewer = $this->getViewer();
 
     $engine = new PhabricatorCalendarNotificationEngine();
+
+    $minutes = $args->getArg('minutes');
+    if ($minutes) {
+      $engine->setNotifyWindow(phutil_units("{$minutes} minutes in seconds"));
+    }
+
     $engine->publishNotifications();
 
     return 0;
diff --git a/src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php b/src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php
new file mode 100644
--- /dev/null
+++ b/src/applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php
@@ -0,0 +1,61 @@
+<?php
+
+final class PhabricatorCalendarEventNotificationView
+  extends Phobject {
+
+  private $viewer;
+  private $event;
+  private $epoch;
+  private $dateTime;
+
+  public function setViewer(PhabricatorUser $viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+  public function getViewer() {
+    return $this->viewer;
+  }
+
+  public function setEvent(PhabricatorCalendarEvent $event) {
+    $this->event = $event;
+    return $this;
+  }
+
+  public function getEvent() {
+    return $this->event;
+  }
+
+  public function setEpoch($epoch) {
+    $this->epoch = $epoch;
+    return $this;
+  }
+
+  public function getEpoch() {
+    return $this->epoch;
+  }
+
+  public function setDateTime(PhutilCalendarDateTime $date_time) {
+    $this->dateTime = $date_time;
+    return $this;
+  }
+
+  public function getDateTime() {
+    return $this->dateTime;
+  }
+
+  public function getDisplayMinutes() {
+    $epoch = $this->getEpoch();
+    $now = PhabricatorTime::getNow();
+    $minutes = (int)ceil(($epoch - $now) / 60);
+    return new PhutilNumber($minutes);
+  }
+
+  public function getDisplayTime() {
+    $viewer = $this->getViewer();
+
+    $epoch = $this->getEpoch();
+    return phabricator_datetime($epoch, $viewer);
+  }
+
+}
diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
--- a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
+++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php
@@ -4,6 +4,7 @@
   extends Phobject {
 
   private $cursor;
+  private $notifyWindow;
 
   public function getCursor() {
     if (!$this->cursor) {
@@ -14,9 +15,64 @@
     return $this->cursor;
   }
 
+  public function setCursor($cursor) {
+    $this->cursor = $cursor;
+    return $this;
+  }
+
+  public function setNotifyWindow($notify_window) {
+    $this->notifyWindow = $notify_window;
+    return $this;
+  }
+
+  public function getNotifyWindow() {
+    if (!$this->notifyWindow) {
+      return phutil_units('15 minutes in seconds');
+    }
+
+    return $this->notifyWindow;
+  }
+
   public function publishNotifications() {
     $cursor = $this->getCursor();
 
+    $now = PhabricatorTime::getNow();
+    if ($cursor > $now) {
+      return;
+    }
+
+    $calendar_class = 'PhabricatorCalendarApplication';
+    if (!PhabricatorApplication::isClassInstalled($calendar_class)) {
+      return;
+    }
+
+    try {
+      $lock = PhabricatorGlobalLock::newLock('calendar.notify')
+        ->lock(5);
+    } catch (PhutilLockException $ex) {
+      return;
+    }
+
+    $caught = null;
+    try {
+      $this->sendNotifications();
+    } catch (Exception $ex) {
+      $caught = $ex;
+    }
+
+    $lock->unlock();
+
+    // Wait a little while before checking for new notifications to send.
+    $this->setCursor($cursor + phutil_units('5 minutes in seconds'));
+
+    if ($caught) {
+      throw $caught;
+    }
+  }
+
+  private function sendNotifications() {
+    $cursor = $this->getCursor();
+
     $window_min = $cursor - phutil_units('16 hours in seconds');
     $window_max = $cursor + phutil_units('16 hours in seconds');
 
@@ -100,7 +156,7 @@
     }
 
     $notify_min = $cursor;
-    $notify_max = $cursor + phutil_units('15 minutes in seconds');
+    $notify_max = $cursor + $this->getNotifyWindow();
     $notify_map = array();
     foreach ($events as $key => $event) {
       $initial_epoch = $event->getUTCInitialEpoch();
@@ -136,11 +192,13 @@
           continue;
         }
 
-        $notify_map[$user_phid][] = array(
-          'event' => $event,
-          'datetime' => $user_datetime,
-          'epoch' => $user_epoch,
-        );
+        $view = id(new PhabricatorCalendarEventNotificationView())
+          ->setViewer($user)
+          ->setEvent($event)
+          ->setDateTime($user_datetime)
+          ->setEpoch($user_epoch);
+
+        $notify_map[$user_phid][] = $view;
       }
     }
 
@@ -149,24 +207,23 @@
     $now = PhabricatorTime::getNow();
     foreach ($notify_map as $user_phid => $events) {
       $user = $user_map[$user_phid];
-      $events = isort($events, 'epoch');
-
-      // TODO: This is just a proof-of-concept that gets dumped to the console;
-      // it will be replaced with a nice fancy email and notification.
 
-      $body = array();
-      $body[] = pht('%s, these events start soon:', $user->getUsername());
-      $body[] = null;
-      foreach ($events as $spec) {
-        $event = $spec['event'];
-        $body[] = $event->getName();
+      $locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());
+      $caught = null;
+      try {
+        $mail_list[] = $this->newMailMessage($user, $events);
+      } catch (Exception $ex) {
+        $caught = $ex;
       }
-      $body = implode("\n", $body);
 
-      $mail_list[] = $body;
+      unset($locale);
 
-      foreach ($events as $spec) {
-        $event = $spec['event'];
+      if ($caught) {
+        throw $ex;
+      }
+
+      foreach ($events as $view) {
+        $event = $view->getEvent();
         foreach ($event->getNotificationPHIDs() as $phid) {
           $mark_list[] = qsprintf(
             $conn,
@@ -192,9 +249,55 @@
     }
 
     foreach ($mail_list as $mail) {
-      echo $mail;
-      echo "\n\n";
+      $mail->saveAndSend();
     }
   }
 
+
+  private function newMailMessage(PhabricatorUser $viewer, array $events) {
+    $events = msort($events, 'getEpoch');
+
+    $next_event = head($events);
+
+    $body = new PhabricatorMetaMTAMailBody();
+    foreach ($events as $event) {
+      $body->addTextSection(
+        null,
+        pht(
+          '%s is starting in %s minute(s), at %s.',
+          $event->getEvent()->getName(),
+          $event->getDisplayMinutes(),
+          $event->getDisplayTime()));
+
+      $body->addLinkSection(
+        pht('EVENT DETAIL'),
+        PhabricatorEnv::getProductionURI($event->getEvent()->getURI()));
+    }
+
+    $next_event = head($events)->getEvent();
+    $subject = $next_event->getName();
+    if (count($events) > 1) {
+      $more = pht(
+        '(+%s more...)',
+        new PhutilNumber(count($events) - 1));
+      $subject = "{$subject} {$more}";
+    }
+
+    $calendar_phid = id(new PhabricatorCalendarApplication())
+      ->getPHID();
+
+    return id(new PhabricatorMetaMTAMail())
+      ->setSubject($subject)
+      ->addTos(array($viewer->getPHID()))
+      ->setSensitiveContent(false)
+      ->setFrom($calendar_phid)
+      ->setIsBulk(true)
+      ->setSubjectPrefix(pht('[Calendar]'))
+      ->setVarySubjectPrefix(pht('[Reminder]'))
+      ->setThreadID($next_event->getPHID(), false)
+      ->setRelatedPHID($next_event->getPHID())
+      ->setBody($body->render())
+      ->setHTMLBody($body->renderHTML());
+  }
+
 }
diff --git a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php
--- a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php
+++ b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php
@@ -20,6 +20,8 @@
   private $nuanceSources;
   private $nuanceCursors;
 
+  private $calendarEngine;
+
   protected function run() {
 
     // The trigger daemon is a low-level infrastructure daemon which schedules
@@ -105,6 +107,7 @@
       $sleep_duration = $this->getSleepDuration();
       $sleep_duration = $this->runNuanceImportCursors($sleep_duration);
       $sleep_duration = $this->runGarbageCollection($sleep_duration);
+      $sleep_duration = $this->runCalendarNotifier($sleep_duration);
       $this->sleep($sleep_duration);
     } while (!$this->shouldExit());
   }
@@ -456,4 +459,21 @@
     return true;
   }
 
+
+/* -(  Calendar Notifier  )-------------------------------------------------- */
+
+
+  private function runCalendarNotifier($duration) {
+    $run_until = (PhabricatorTime::getNow() + $duration);
+
+    if (!$this->calendarEngine) {
+      $this->calendarEngine = new PhabricatorCalendarNotificationEngine();
+    }
+
+    $this->calendarEngine->publishNotifications();
+
+    $remaining = max(0, $run_until - PhabricatorTime::getNow());
+    return $remaining;
+  }
+
 }