1 | <?php␊ |
2 | ␊ |
3 | namespace Fuel\Tasks;␊ |
4 | ␊ |
5 | class Pannous {␊ |
6 | private $gpg;␊ |
7 | ␊ |
8 | private function checkMailSignature($mbox, $message_number, $mail, $user)␊ |
9 | {␊ |
10 | ␉$structure = imap_fetchstructure($mbox, $message_number);␊ |
11 | ␊ |
12 | ␉if (!$structure->ifsubtype ||␊ |
13 | ␉ strtolower($structure->subtype) != "signed")␊ |
14 | return false;␊ |
15 | ␊ |
16 | ␉if (!$structure->parts ||␊ |
17 | ␉ count($structure->parts) != 2)␊ |
18 | return false;␊ |
19 | ␊ |
20 | ␉if (!$structure->parts[1]->ifsubtype ||␊ |
21 | ␉ strtolower($structure->parts[1]->subtype) != "pgp-signature")␊ |
22 | return false;␊ |
23 | ␊ |
24 | ␉if ($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 | ␊ |
31 | ␉if (!$this->gpg)␊ |
32 | return false;␊ |
33 | ␊ |
34 | ␉if (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 | }␊ |
46 | ␉else␊ |
47 | $public_key = $user->public_key;␊ |
48 | ␊ |
49 | ␉$res_import = gnupg_import($this->gpg, $public_key);␊ |
50 | ␉if ($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-----");␊ |
62 | ␉if ($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 | ␊ |
69 | ␉return $res_verify !== false && $res_verify[0]["fingerprint"] == $res_import["fingerprint"];␊ |
70 | }␊ |
71 | ␊ |
72 | private function getHeader($headers, $target)␊ |
73 | {␊ |
74 | ␉for ($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 | }␊ |
89 | ␉return null;␊ |
90 | }␊ |
91 | ␊ |
92 | private function propagateMail($mbox, $message_number, $mail, $list)␊ |
93 | {␊ |
94 | ␉$readers = $list->getReaders(false);␊ |
95 | ␊ |
96 | ␉if (!$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");␊ |
122 | ␉array_push($headers, $content_type);␊ |
123 | ␊ |
124 | ␉$subject = $this->getHeader($raw_headers_exploded, "Subject");␊ |
125 | ␉if ($subject)␊ |
126 | $subject = substr($subject, strlen("Subject: "), strlen($subject));␊ |
127 | ␉else␊ |
128 | $subject = "";␊ |
129 | ␊ |
130 | ␉$structure = imap_fetchstructure($mbox, $message_number);␊ |
131 | ␉$body = imap_body($mbox, $message_number);␊ |
132 | ␊ |
133 | ␉for($i=0; $i<count($headers); $i++)␊ |
134 | {␊ |
135 | $headers[$i] = trim($headers[$i]);␊ |
136 | }␊ |
137 | ␉␊ |
138 | ␉foreach($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);␊ |
159 | ␉foreach($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))␊ |
166 | ␉␉return $text;␊ |
167 | ␉}␊ |
168 | ␊ |
169 | ␉return null;␊ |
170 | }␊ |
171 | ␊ |
172 | private function extract_subject($str)␊ |
173 | {␊ |
174 | ␉$elements = imap_mime_header_decode($str);␊ |
175 | ␉$res = "";␊ |
176 | ␉foreach($elements as $element)␊ |
177 | ␉{␊ |
178 | ␉ if ($res) $res .= " ";␊ |
179 | ␉ $res .= $element->text;␊ |
180 | ␉}␊ |
181 | ␊ |
182 | ␉return $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 | ␊ |
191 | ␉if ($mbox == FALSE)␊ |
192 | ␉ die("Unable to open " . \Config::get('pannous.mail_server') . " " . imap_last_error());␊ |
193 | ␊ |
194 | ␉$mbox_status = imap_check($mbox);␊ |
195 | ␊ |
196 | ␉if ($mbox == FALSE)␊ |
197 | ␉ die("Unable to status mailbox " . imap_last_error());␊ |
198 | ␊ |
199 | ␉if (!$mbox_status->Nmsgs)␊ |
200 | ␉{␊ |
201 | imap_close($mbox);␊ |
202 | return 0;␊ |
203 | ␉}␊ |
204 | ␊ |
205 | ␉$mailList = imap_fetch_overview($mbox, "1:".$mbox_status->Nmsgs);␊ |
206 | ␉foreach ($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 . ")");␊ |
217 | ␉␉imap_delete ($mbox, $mail->msgno);␊ |
218 | ␉␉continue;␊ |
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");␊ |
230 | ␉␉imap_delete ($mbox, $mail->msgno);␊ |
231 | ␉␉continue;␊ |
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 | ␉ {␊ |
246 | ␉␉if (!$user)␊ |
247 | ␉␉{␊ |
248 | ␉␉ $user = Model_Users::create_user(␊ |
249 | ␉␉␉$from,␊ |
250 | ␉␉␉"", /* force password generation */␊ |
251 | ␉␉␉$from,␊ |
252 | ␉␉␉Model_Users::$ROLE_USER␊ |
253 | ␉␉ );␊ |
254 | ␉␉}␊ |
255 | ␉␉/* Already reader ? */␊ |
256 | ␉␉if (!$list->isReader($user))␊ |
257 | ␉␉{␊ |
258 | ␉␉ \Log::info("Mail subscribe " . $user->email . " for " . $list->email);␊ |
259 | ␉␉ $list->sendConfirmationEmail($user);␊ |
260 | ␉␉ $list->addReader($user);␊ |
261 | ␉␉}␊ |
262 | ␉␉imap_delete ($mbox, $mail->msgno);␊ |
263 | ␉␉continue;␊ |
264 | ␉ }␊ |
265 | ␉ else if (!strcasecmp($subject, "unsubscribe"))␊ |
266 | ␉ {␊ |
267 | ␉␉if ($user && $list->isReader($user))␊ |
268 | ␉␉{␊ |
269 | ␉␉ \Log::info("Mail unsubscribe " . $user->email . " for " . $list->email);␊ |
270 | ␉␉ $list->unsubscribeUser($user);␊ |
271 | ␉␉}␊ |
272 | ␉␉$list->sendUnsubscribeEmail($user);␊ |
273 | ␉␉imap_delete ($mbox, $mail->msgno);␊ |
274 | ␉␉continue;␊ |
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 | ␊ |
334 | ␉imap_close($mbox, CL_EXPUNGE);␊ |
335 | }␊ |
336 | ␊ |
337 | public static function run()␊ |
338 | {␊ |
339 | ␉$wj = new Pannous();␊ |
340 | ␉$wj->_run();␊ |
341 | }␊ |
342 | }␊ |
343 | ?> |