+}
+
+// }}}
+
+// {{{ class NLIssue
+
+// A NLIssue is an issue of a given NewsLetter
+class NLIssue
+{
+ protected $nlid; // Id of the newsletter
+
+ const STATE_NEW = 'new'; // New, currently being edited
+ const STATE_PENDING = 'pending'; // Ready for mailing
+ const STATE_SENT = 'sent'; // Sent
+
+ public $nl; // Related NL
+
+ public $id; // Id of this issue of the newsletter
+ public $shortname; // Shortname for this issue
+ public $title; // Title of this issue
+ public $title_mail; // Title of the email
+ public $state; // State of the issue (one of the STATE_ values)
+ public $sufb; // Environment to use to generate the UFC through an UserFilterBuilder
+
+ public $date; // Date at which this issue was sent
+ public $send_before; // Date at which issue should be sent
+ public $head; // Foreword of the issue (or body for letters with no articles)
+ public $signature; // Signature of the letter
+ public $reply_to; // Adress to reply to the message (can be empty)
+ public $arts = array(); // Articles of the issue
+
+ const BATCH_SIZE = 60; // Number of emails to send every minute.
+
+ // {{{ Constructor, id-related functions
+
+ /** Build a NewsLetter.
+ * @p $id: ID of the issue (unique among all newsletters)
+ * @p $nl: Optional argument containing an already built NewsLetter object.
+ */
+ function __construct($id, $nl = null, $fetch_articles = true)
+ {
+ return $this->fetch($id, $nl, $fetch_articles);
+ }
+
+ protected function refresh()
+ {
+ return $this->fetch($this->id, $this->nl, false);
+ }
+
+ protected function fetch($id, $nl = null, $fetch_articles = true)
+ {
+ // Load this issue
+ $res = XDB::query('SELECT nlid, short_name, date, send_before, state, sufb_json,
+ title, mail_title, head, signature, reply_to
+ FROM newsletter_issues
+ WHERE id = {?}',
+ $id);
+ if (!$res->numRows()) {
+ throw new MailNotFound();
+ }
+ $issue = $res->fetchOneAssoc();
+ if ($nl && $nl->id == $issue['nlid']) {
+ $this->nl = $nl;
+ } else {
+ $this->nl = new NewsLetter($issue['nlid']);
+ }
+ $this->id = $id;
+ $this->nlid = $issue['nlid'];
+ $this->shortname = $issue['short_name'];
+ $this->date = $issue['date'];
+ $this->send_before = $issue['send_before'];
+ $this->state = $issue['state'];
+ $this->title = $issue['title'];
+ $this->title_mail = $issue['mail_title'];
+ $this->head = $issue['head'];
+ $this->signature = $issue['signature'];
+ $this->reply_to = $issue['reply_to'];
+ $this->sufb = $this->importJSonStoredUFB($issue['sufb_json']);
+
+ if ($fetch_articles) {
+ $this->fetchArticles();
+ }
+ }
+
+ protected function fetchArticles($force = false)
+ {
+ if (count($this->arts) && !$force) {
+ return;
+ }
+
+ // Load the articles
+ $res = XDB::iterRow(
+ 'SELECT a.title, a.body, a.append, a.aid, a.cid, a.pos
+ FROM newsletter_art AS a
+ INNER JOIN newsletter_issues AS ni USING(id)
+ LEFT JOIN newsletter_cat AS c ON (a.cid = c.cid)
+ WHERE a.id = {?}
+ ORDER BY c.pos, a.pos',
+ $this->id);
+ while (list($title, $body, $append, $aid, $cid, $pos) = $res->next()) {
+ $this->arts[$cid][$aid] = new NLArticle($title, $body, $append, $aid, $cid, $pos);
+ }
+ }
+
+ protected function importJSonStoredUFB($json = null)
+ {
+ require_once 'ufbuilder.inc.php';
+ $ufb = $this->nl->getSubscribersUFB();
+ if (is_null($json)) {
+ return new StoredUserFilterBuilder($ufb, new PFC_True());
+ }
+ $export = json_decode($json, true);
+ if (is_null($export)) {
+ PlErrorReport::report("Invalid json while reading NL {$this->nlid}, issue {$this->id}: failed to import '''{$json}'''.");
+ return new StoredUserFilterBuilder($ufb, new PFC_True());
+ }
+ $sufb = new StoredUserFilterBuilder($ufb);
+ $sufb->fillFromExport($export);
+ return $sufb;
+ }
+
+ protected function exportStoredUFBAsJSon()
+ {
+ return json_encode($this->sufb->export());
+ }
+
+ public function id()