fromname; $email = $this->fromemail; return "\"$name\" <$email>"; } } class DefaultMailing extends BaseMailing { private $user; public function __construct(User $user) { $this->user = $user; } public function getFromname() { return $this->user->name; } public function getFromemail() { return $this->user->emailaddress; } public function getSubject() { return "onderwerp"; } public function getBody() { return ""; } } class Mailing extends BaseMailing { /** @inject @var \App\Services\SendmailMailer */ public $mailer; public static function create(\App\Services\Database $database, $params = []) { $user = is_null(@$params['user']) ? null : $params['user']->primary; $usergroup = is_null(@$params['usergroup']) ? null : $params['usergroup']->primary; $addresslist = is_null(@$params['addresslist']) ? null : $params['addresslist']->primary; $context = $database->context; $activerow = $context->table('mailings')->insert([ 'mailsubject' => @$params['subject'], 'mailfromname' => @$params['fromname'], 'mailfromemail' => @$params['fromemail'], 'mailfromtoken' => bin2hex(openssl_random_pseudo_bytes(16)), 'addresslist' => $addresslist, 'luser' => $user, 'lusergroup' => $usergroup, ]); $body = is_null(@$params['body']) ? '' : $params['body']; $context->table('mailbodies')->insert([ 'mailing' => $activerow->mailing, 'bodytext' => $body, ]); return $database->createModel('Mailing', $activerow); } public function createClone() { return Mailing::create($this->database, [ 'subject' => $this->getSubject(), 'fromname' => $this->getFromname(), 'fromemail' => $this->getFromemail(), 'addresslist' => $this->getAddresslist(), 'user' => $this->getUser(), 'usergroup' => $this->getUsergroup(), 'body' => $this->getBody(), ]); } public function delete() { $this->activerow->delete(); } public function getCtime() { return $this->activerow->ctime; } public function getMtime() { $mine = $this->activerow->mtime; $addresslist = $this->addresslist; if($addresslist) return max($mine, $addresslist->mtime); return $mine; } public function getMailingdate() { return $this->getCtime(); } public function getApproved() { return $this->activerow->approved_when > $this->getMtime(); } public function getSent() { return $this->activerow->sent_when > $this->getMtime(); } public function getSubmitted() { return $this->activerow->submitted_when > $this->getMtime(); } public function getRejected() { return $this->activerow->rejected_when > $this->getMtime(); } public function getFromapproved() { return $this->activerow->mailfromapproved; } public function getToken() { return $this->activerow->mailfromtoken; } public function setToken($token) { return $this->activerow->update(['mailfromtoken' => $token]); } // NB: getStage() en getIncomplete() zijn voor gebruik in templates // Dus *niet* voor de rest van de code als basis om beslissingen te nemen. // Om dat te doen zou het gerefactord moeten worden naar een klassehierarchie. // (Visitor pattern) public function getStage() { $rows = $this->activerow->related('finaldestinations')->where('sent_when IS NULL')->limit(1); foreach($rows as $row) return 'sending'; $rows = $this->activerow->related('finaldestinations')->limit(1); foreach($rows as $row) return 'sent'; if($this->getRejected()) return 'rejected'; if($this->getApproved()) return 'approved'; if($this->getSubmitted()) return 'submitted'; if($this->getProofread()) return 'proofread'; if($this->getIncomplete()) return 'draft'; return 'complete'; } // zie comment bij getStage() public function getIncomplete() { $incomplete = []; if(empty($this->getFromname())) $incomplete[] = 'from-name'; if(empty($this->getFromemail())) $incomplete[] = 'from-mail'; if (!$this->getFromapproved()) { $incomplete[] = 'from-mail-not-approved'; } if(empty($this->getSubject())) $incomplete[] = 'subject'; if(empty($this->getBody())) $incomplete[] = 'body'; $addresslist = $this->getAddresslist(); if($addresslist) { if(!$addresslist->hasAddresses()) $incomplete[] = 'addresslist-empty'; } else { $incomplete[] = 'addresslist'; } return $incomplete; } public function getProofread() { return $this->activerow->proofread_when > $this->getMtime(); } public function getAddressapproved() { return $this->activerow->mailfromapproved; } public function setAddressapproved($state = true) { return $this->activerow->update(['mailfromapproved' => $state]); } private $attachments; public function getAttachments() { if (!is_null($this->attachments)) return $this->attachments; $database = $this->database; $rows = $this->activerow->related('attachments'); $attachments = []; foreach($rows as $row) { $attachments[] = $database->createModel('Attachment', $row); } return $this->attachments = $attachments; } private $attachment; public function get_attachment($id) { if (!is_null($this->attachment)) return $this->attachment; $database = $this->database; $rows = $this->activerow->related('attachments')->where('attachment', $id); foreach($rows as $row) { return $database->createModel('Attachment', $row); } return null; } public function addAttachments($attachments) { $luser = $this->activerow->luser; $results = []; // beschikbaar voor Nette\Http\FileUpload: name, type, size, tmpName, error foreach($attachments as $attachment) { $contents = file_get_contents($attachment->TemporaryFile); $download = 0; if ($attachment->size > $this->cfg->attachment_warnsize) { $download = 1; $results[] = "$attachment->name is a relative large file hence we didn't attach it to the message but created a downloadlink."; } $att=Attachment::create($this->database, [ 'user' => $luser, 'mailing' => $this, 'filename' => $attachment->name, 'size' => $attachment->size, 'download' => $download, 'contents' => $contents ]); } return $results; } public function getBody() { $rows = $this->activerow->related('mailbodies'); # should only return one row, but anyway… foreach($rows as $row) return $row->bodytext; return ""; } private static $sanitize = <<<'XSL'
XSL; public function getSanitized_body() { $insane = new \DOMDocument('1.0', 'UTF-8'); $insane->recover = true; $insane->resolveExternals = false; $insane->strictErrorChecking = false; @$insane->loadHTML($this->getBody()); $xml = new \DOMDocument; $xml->loadXML(self::$sanitize); $xsl = new \XSLTProcessor; $xsl->importStylesheet($xml); $sane = $xsl->transformToDoc($insane); return $sane->saveHTML(); } public function setBody($text) { // TODO: hier aanroepen van checkBody($text) of beter in de presenter laten? $rows = $this->activerow->related('mailbodies'); $this->activerow->update(['mtime' => new \DateTime]); # should only return one row, but anyway… foreach($rows as $row) return $row->update(['bodytext' => $text]); } public function checkBody($text) { // TODO: Check body and attachments ($cfg>message_maxsize MB) en presence of all // attachments (and no links to non-existing ones binnen "attachmentdownloadurl") $errors = []; return $errors; } public function getRejection() { return $this->activerow->rejection; } public function getFromname() { return $this->activerow->mailfromname; } public function setFromname($name) { return $this->activerow->update(['mailfromname' => $name, 'mtime' => new \DateTime]); } public function getFromemail() { return $this->activerow->mailfromemail; } public function setFromemail($email) { return $this->activerow->update(['mailfromemail' => $email, 'mtime' => new \DateTime]); } public function getSubject() { return $this->activerow->mailsubject; } public function setSubject($subject) { return $this->activerow->update(['mailsubject' => $subject, 'mtime' => new \DateTime]); } public function getAddresslist() { $row = $this->activerow->ref('addresslist'); return $row ? $this->database->createModel('Addresslist', $row) : null; } public function setAddresslist($addresslist) { $this->activerow->update(['addresslist' => $addresslist->primary, 'mtime' => new \DateTime]); } public function getOwner() { return $this->activerow->luser; } public function getOwnergroup() { return $this->activerow->lusergroup; } public function getUser() { $row = $this->activerow->ref('luser'); return $row ? $this->database->createModel('User', $row) : null; } public function setUser($user) { $this->activerow->update(['luser' => $user->primary]); } public function getUsergroup() { $row = $this->activerow->ref('lusergroup'); return $row ? $this->database->createModel('Usergroup', $row) : null; } public function setUsergroup($usergroup) { $this->activerow->update(['lusergroup' => $usergroup->primary]); } public function sendTestMail($luser) { $message = $this->as_message(); $message->addTo($luser->emailaddress) ->setSubject("[PROOFREAD] ".$this->getSubject()); $this->mailer->send($message); $this->activerow->update(['proofread_when' => new \DateTime]); } public function submitForApproval($luser) { if (!$this->getProofread() || $this->getSubmitted()) { throw new \ErrorException("Mailing not in the proper state for submission."); } $cfg = $this->cfg; $message = $this->mailer->createMessage() ->setEnvelopeFrom($this->cfg->helpdeskemailaddress) ->setFrom($luser->emailaddress) ->addTo($cfg->moderators) ->setSubject("[SUBMISSION] ".$this->getSubject()) ->setBody("$luser->name created a mailing. Please verify."); $this->mailer->send($message); $this->activerow->update(['submitted_when' => new \DateTime]); } public function reject($luser, $mtime, $reason) { if ((string)$this->mtime !== (string)$mtime) { throw new \ErrorException("Mailing changed before approval finished."); } if (!$this->getSubmitted() || $this->getApproved() || $this->getRejected()) { throw new \ErrorException("Mailing not in the proper state for rejection."); } $this->activerow->update(['rejected_when' => new \DateTime, 'rejection' => $reason]); $this->sendmessagetouser( $luser, $this->getFrom(), # FIXME: Moet worden: de mailing->luser (bv vanwege noreply@) "{$this->templateDir}/reject.latte"); } public function approve($luser, $mtime) { if ((string)$this->mtime !== (string)$mtime) { throw new \ErrorException("Mailing changed before approval finished."); } if (!$this->getSubmitted() || $this->getApproved() || $this->getRejected()) { throw new \ErrorException("Mailing not in the proper state for approval."); } $this->sendmessagetouser( $luser, $this->getFrom(), # FIXME: Moet worden: de mailing->luser (bv vanwege noreply@) "{$this->templateDir}/approve.latte"); $this->activerow->update(['approved_when' => new \DateTime]); } /** * @return 0|1 result of message-sent (no|yes) */ public function requestAddresschange($luser, $mtime) { if ((string)$this->mtime !== (string)$mtime) { throw new \ErrorException("Mailing changed before request was sent."); } // Generate a new token here to prevent abuse $this->activerow->update(['mailfromapproved' => false, 'mailfromtoken' => bin2hex(openssl_random_pseudo_bytes(16))]); # FIXME: Future version: use mailfromtoken_expires to prevent too long changeable field $this->sendmessagetouser( $luser, $this->getFromemail(), "{$this->templateDir}/addresschange.latte"); return 1; } public function addresschangeapproved($luser, $token) { if ($this->activerow->mailfromtoken != $token) { return "bad-token"; } if ($this->activerow->mailfromapproved) { // No need for approval, do not send a message return "no-approval-necessary"; } // Accept entry and invalidate token $this->activerow->update(['mailfromapproved' => true, 'mailfromtoken' => bin2hex(openssl_random_pseudo_bytes(16))]); $this->sendmessagetouser( $luser, $this->getFromemail(), "{$this->templateDir}/addresschangeok.latte" ); return "ok"; } public function addresschangerejected($luser, $token) { if ($this->activerow->mailfromtoken != $token) { return "bad-token"; } if ($this->activerow->mailfromapproved) { // No need for approval, do not send a message return "no-approval-necessary"; } // Accept entry and invalidate token $this->activerow->update(['mailfromtoken' => bin2hex(openssl_random_pseudo_bytes(16))]); $this->sendmessagetouser( $luser, $this->getFromemail(), "{$this->templateDir}/addresschangedenied.latte" ); // Since this address is denied we change the address here to a NULL address // (change here because we need to send a message first) $this->activerow->update(['mailfromemail' => NULL, 'mailfromapproved' => true]); return "ok"; } private function sendmessagetouser($luser, $to, $messagetemplate, $mailtemplate=NULL) { $cfg = $this->cfg; $messagecontent = new Nette\Templating\FileTemplate("$messagetemplate"); $messagecontent->registerFilter(new Nette\Latte\Engine); $messagecontent->luser = $luser; $messagecontent->mailing = $this; $messagecontent->cfg = $cfg; if($mailtemplate) { $messagetext = new Nette\Templating\FileTemplate("$mailtemplate"); } else { $messagetext = new Nette\Templating\FileTemplate("{$this->templateDir}/mailtemplate.tpl"); } $messagetext->registerFilter(new Nette\Latte\Engine); $messagetext->content = (string)$messagecontent; # Leave subject out (NULL) to use from the template (SUBJECT) $message = $this->mailer->createMessage() // FIXME addEmbeddedFile toevoegen aan App\Services\MailMessage om logos in mail mogelijk te maken // ->addEmbeddedFile('logo', file_get_contents("{$this->templateDir}/logo-smaller.png"), 'image/png') // ->addEmbeddedFile('slogan', file_get_contents("{$this->templateDir}/slogan-smaller.png"), 'image/png') ->setEnvelopeFrom($cfg->helpdeskemailaddress) ->setFrom($luser->emailaddress) ->addReplyTo($cfg->administrativefrom) ->addTo($to) ->setHTMLBody((string)$messagetext); $this->mailer->send($message); } public function send() { if(!$this->getApproved() || $this->getSent()) throw new \ErrorException("Mailing not in the proper state for sending."); $this->activerow->update(['sent_when' => new \DateTime]); $finaldestinations = $this->context->table('finaldestinations'); $addresslist = $this->getAddresslist(); $primary = $this->getPrimary(); foreach($addresslist->addresses as $key => $value) { $finaldestinations->insert([ 'mailing' => $primary, 'emailaddress' => $value->primary, 'actual_emailaddress' => $value->address, ]); } } public function as_message() { $message = $this->mailer->createMessage() ->setFrom($this->getFrom()) ->setEnvelopeFrom($this->cfg->helpdeskemailaddress) ->setSubject($this->getSubject()) ->setHtmlBody($this->getBody()); $attachments = $this->getAttachments(); foreach($attachments as $attachment) if(!$attachment->download) $message->addAttachment($attachment->filename, $attachment->contents); return $message; } private $cache_bounces; public function getBounces() { if (!$this->getSent()) return []; if (!is_null($this->cache_bounces)) return $this->cache_bounces; $bounces = []; $rows = $this->activerow->related('finaldestinations')->where('last_bounce_received_when IS NOT NULL'); foreach($rows as $row) { $bounces[] = [ 'emailaddress' => $row['actual_emailaddress'], 'reason' => "Bounced message" # FIXME: reden uit subject halen? ]; } return $this->cache_bounces = $bounces; } }