Pannous

Pannous Git Source Tree

Root/fuel/app/tasks/pannous.php

1<?php
2
3namespace Fuel\Tasks;
4
5class Pannous {
6 private $gpg;
7
8 private function checkMailSignature($mbox, $message_number, $mail, $user)
9 {
10$structure = imap_fetchstructure($mbox, $message_number);
11
12if (!$structure->ifsubtype ||
13 strtolower($structure->subtype) != "signed")
14 return false;
15
16if (!$structure->parts ||
17 count($structure->parts) != 2)
18 return false;
19
20if (!$structure->parts[1]->ifsubtype ||
21 strtolower($structure->parts[1]->subtype) != "pgp-signature")
22 return false;
23
24if ($this->gpg == null)
25 {
26 putenv('GNUPGHOME=' . \Config::get('pannous.gpg_dir'));
27 $this->gpg = gnupg_init();
28 gnupg_setarmor($this->gpg, true);
29 }
30
31if (!$this->gpg)
32 return false;
33
34if (strpos($user->public_key, "http") === 0)
35 {
36 $public_key = file_get_contents($user->public_key);
37
38 $start = strpos($public_key, "-----BEGIN PGP PUBLIC KEY BLOCK-----");
39 $end = strpos($public_key, "-----END PGP PUBLIC KEY BLOCK-----");
40
41 if ($start === false || $end === false)
42 return false;
43
44 $public_key = substr($public_key, $start, $end+strlen("-----END PGP PUBLIC KEY BLOCK-----"));
45 }
46else
47 $public_key = $user->public_key;
48
49$res_import = gnupg_import($this->gpg, $public_key);
50if ($res_import ===false || (
51 $res_import["imported"] == 0 && $res_import["unchanged"] == 0))
52 return false;
53
54/* Need to add MIME headers for verification */
55$signed_text = imap_fetchmime($mbox, $message_number, 1, FT_INTERNAL);
56$signed_text = $signed_text . imap_fetchbody($mbox, $message_number, 1, FT_INTERNAL);
57
58$signature = imap_fetchbody($mbox, $message_number, 2);
59
60$start = strpos($signature, "-----BEGIN PGP SIGNATURE-----");
61$end = strpos($signature, "-----END PGP SIGNATURE-----");
62if ($start === false || $end === false)
63 return false;
64
65$signature = substr($signature, $start, $end+strlen("-----END PGP SIGNATURE-----"));
66
67$res_verify = gnupg_verify($this->gpg, $signed_text, $signature);
68
69return $res_verify !== false && $res_verify[0]["fingerprint"] == $res_import["fingerprint"];
70 }
71
72 private function getHeader($headers, $target)
73 {
74for ($i=0; $i<count($headers); $i++)
75 {
76 if (strpos($headers[$i], $target) === 0)
77 {
78 $res = $headers[$i];
79 /* Handle multiple line header*/
80 for ($a = $i+1; $a<count($headers); $a++)
81 {
82 if (strpos($headers[$a], ':') != false)
83 break;
84 $res = $res . $headers[$a];
85 }
86 return $res;
87 }
88 }
89return null;
90 }
91
92 private function propagateMail($mbox, $message_number, $mail, $list)
93 {
94$readers = $list->getReaders(false);
95
96if (!$readers)
97 {
98 \Log::debug("No readers");
99 return;
100 }
101
102$raw_headers = imap_fetchheader($mbox, $message_number);
103$raw_headers_exploded = explode("\n", $raw_headers);
104
105$headers = array(
106 "From: " . $list->email,
107 "List-Id: <" . \Config::get('base_url') . "/lists/" . $list->id . ">",
108 "List-URL: <" . \Config::get('base_url') . "/lists/" . $list->id . ">",
109 "List-Post: <mailto:" . $list->email . ">",
110 "List-Subscribe: <mailto:" . $list->email . "?subject=Subscribe>",
111 "List-Unsubscribe: <mailto:" . $list->email . "?subject=Unsubscribe>",
112 //"List-Subscribe: <" . \Config::get('base_url') . "/lists/" . $list->id . "/subscribe>",
113
114 "Message-ID: <" . sha1(date("U.u") . $mail->from) . "@" . $list->email . ">", // U is seconds since 1900 and u is microseconds
115 "Resent-Message-ID: " . $mail->message_id,
116 "MIME-Version: 1.0",
117 "Resent-From: " . $list->email,
118 "X-Loop: " . $list->email
119);
120
121$content_type = $this->getHeader($raw_headers_exploded, "Content-Type");
122array_push($headers, $content_type);
123
124$subject = $this->getHeader($raw_headers_exploded, "Subject");
125if ($subject)
126 $subject = substr($subject, strlen("Subject: "), strlen($subject));
127else
128 $subject = "";
129
130$structure = imap_fetchstructure($mbox, $message_number);
131$body = imap_body($mbox, $message_number);
132
133for($i=0; $i<count($headers); $i++)
134 {
135 $headers[$i] = trim($headers[$i]);
136 }
137
138foreach($readers as $user)
139 {
140 \Log::debug("Propagate to " . $user->email);
141 $target_headers = $headers;
142 /* echo "======================\n"; */
143 /* echo "==> To " . $user->email . "\r\n"; */
144 /* echo "==> Subject " . $subject . "\r\n"; */
145 /* echo "B======================B\n"; */
146 /* echo $body . "\r\n"; */
147 /* echo "H======================H\n"; */
148 /* echo implode("\r\n", $target_headers); */
149 /* echo "\n"; */
150 /* echo "E======================E\n"; */
151
152 mail($user->email, $subject, $body, implode("\r\n", $target_headers));
153 }
154 }
155
156 private function extract_mail($str)
157 {
158$elements = imap_mime_header_decode($str);
159foreach($elements as $element)
160{
161 $text = $element->text;
162 $text = str_replace('<', '', $text);
163 $text = str_replace('>', '', $text);
164 $text = trim($text);
165 if (filter_var($text, FILTER_VALIDATE_EMAIL))
166return $text;
167}
168
169return null;
170 }
171
172 private function extract_subject($str)
173 {
174$elements = imap_mime_header_decode($str);
175$res = "";
176foreach($elements as $element)
177{
178 if ($res) $res .= " ";
179 $res .= $element->text;
180}
181
182return $res;
183 }
184
185 public function _run()
186 {
187$mbox = imap_open(\Config::get('pannous.mail_server'),
188 \Config::get('pannous.mail_username'),
189 \Config::get('pannous.mail_password'));
190
191if ($mbox == FALSE)
192 die("Unable to open " . \Config::get('pannous.mail_server') . " " . imap_last_error());
193
194$mbox_status = imap_check($mbox);
195
196if ($mbox == FALSE)
197 die("Unable to status mailbox " . imap_last_error());
198
199if (!$mbox_status->Nmsgs)
200{
201 imap_close($mbox);
202 return 0;
203}
204
205$mailList = imap_fetch_overview($mbox, "1:".$mbox_status->Nmsgs);
206foreach ($mailList as $mail) {
207
208 \Log::debug("New mail for mailing " . $mail->to);
209
210 $from = $this->extract_mail($mail->from);
211 $to = $this->extract_mail($mail->to);
212
213 if (!$from || !$to)
214 {
215\Log::error("Unable to extract data (" . $mail->from . ") and (" . $mail->to . ")");
216\Log::error("Res (" . $from . ") and (" . $to . ")");
217imap_delete ($mbox, $mail->msgno);
218continue;
219 }
220
221 $list = \Model_Lists::query()
222->where('email', $to)
223->related('users')
224->related('groups')
225->get_one();
226
227 if (!$list)
228 {
229\Log::debug("No list associated");
230imap_delete ($mbox, $mail->msgno);
231continue;
232 }
233
234 $has_right = false;
235 $signature_ok = true;
236
237 $user = \Model_Users::query()
238->where('email', $from)
239->get_one();
240
241
242 $subject = $this->extract_subject($mail->subject);
243
244 if (!strcasecmp($subject, "subscribe"))
245 {
246if (!$user)
247{
248 $user = Model_Users::create_user(
249$from,
250"", /* force password generation */
251$from,
252Model_Users::$ROLE_USER
253 );
254}
255/* Already reader ? */
256if (!$list->isReader($user))
257{
258 \Log::info("Mail subscribe " . $user->email . " for " . $list->email);
259 $list->sendConfirmationEmail($user);
260 $list->addReader($user);
261}
262imap_delete ($mbox, $mail->msgno);
263continue;
264 }
265 else if (!strcasecmp($subject, "unsubscribe"))
266 {
267if ($user && $list->isReader($user))
268{
269 \Log::info("Mail unsubscribe " . $user->email . " for " . $list->email);
270 $list->unsubscribeUser($user);
271}
272$list->sendUnsubscribeEmail($user);
273imap_delete ($mbox, $mail->msgno);
274continue;
275 }
276
277 switch($list->write_mode)
278 {
279 case \Model_Lists::$WRITE_WRITERS:
280 $has_right = $user && $user->valid && $list->isWriter($user);
281 break;
282 case \Model_Lists::$WRITE_SIGNED_WRITERS:
283 $has_right = $user && $user->valid && $list->isWriter($user);
284 if ($has_right)
285 $signature_ok = $this->checkMailSignature($mbox, $mail->msgno, $mail, $user);
286 break;
287 case \Model_Lists::$WRITE_READERS:
288 $has_right = $user && $user->valid && $list->isReader($user);
289 break;
290 case \Model_Lists::$WRITE_SIGNED_READERS:
291 $has_right = $user && $user->valid && $list->isReader($user);
292 if ($has_right)
293 $signature_ok = $this->checkMailSignature($mbox, $mail->msgno, $mail, $user);
294 break;
295 case \Model_Lists::$WRITE_VALID_SIGNED_USER:
296 $has_right = $user && $user->valid;
297 if ($has_right)
298 $signature_ok = $this->checkMailSignature($mbox, $mail->msgno, $mail, $user);
299 break;
300 case \Model_Lists::$WRITE_VALID_USER:
301 $has_right = $user && $user->valid;
302 break;
303 case \Model_Lists::$WRITE_EVERYONE:
304 $has_right = true;
305 break;
306 }
307
308 $headers = "From: " . $mail->to;
309 if (!$has_right && $user)
310 {
311 \Log::debug("New mail from " . $mail->from . " to " . $mail->to);
312 \Log::debug("Insufficient rights");
313 mail($mail->from, "Permission forbidden", "You\"re not allowed to write on this mailing list", $headers);
314 }
315 else if (!$signature_ok)
316 {
317 \Log::debug("New mail from " . $mail->from . " to " . $mail->to);
318 \Log::debug("Invalid signature");
319 mail($mail->from, "Signature check failed", "Your mail has been rejected due to signature check fails", $headers);
320 }
321 else if ($has_right && $signature_ok)
322 {
323 \Log::info("New mail from " . $mail->from . " to " . $mail->to);
324 $this->propagateMail($mbox, $mail->msgno, $mail, $list);
325 }
326 /* No user */
327 else
328 {
329\Log::debug("User " . $mail->from . " not referenced for this mailing list");
330 }
331 imap_delete ($mbox, $mail->msgno);
332}
333
334imap_close($mbox, CL_EXPUNGE);
335 }
336
337 public static function run()
338 {
339$wj = new Pannous();
340$wj->_run();
341 }
342}
343?>

Archive Download this file

Branches