1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 * Pan - A Newsreader for Gtk+
4 * Copyright (C) 2002-2006 Charles Kerr <charles@rebelbase.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 2 of the License.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 #include <config.h>
21 #include <ctype.h>
22 #include <string.h>
23 #include <glib.h>
24 extern "C" {
25 #include <glib/gi18n.h>
26 }
27 #include <pan/general/debug.h>
28 #include <pan/general/macros.h>
29 #include <pan/general/string-view.h>
30 #include "gnksa.h"
31 #include "message-check.h"
32 #include "text-massager.h"
33 #include "mime-utils.h"
34
35 using namespace pan;
36
37 /***
38 **** PRIVATE UTILITIES
39 ***/
40
41 namespace
42 {
43 typedef std::set<std::string> unique_strings_t;
44 typedef std::vector<std::string> strings_t;
45
46 // Find and return a list of NNTP groups to send to
47 void
get_nntp_rcpts(const StringView & to,quarks_t & setme)48 get_nntp_rcpts (const StringView& to, quarks_t& setme)
49 {
50 StringView token, myto(to);
51 while (myto.pop_token (token, ','))
52 setme.insert (token.to_string());
53 }
54
55 /***
56 **** OUTGOING MESSAGE CHECKS
57 ***/
58
59 #if 0
60 std::string
61 strip_attribution_and_signature (const StringView& body_in, GMimeMessage * message)
62 {
63 std::string body (body_in.to_string());
64
65 // strip attribution
66 const char * attribution = g_mime_object_get_header ((GMimeObject *) message, PAN_ATTRIBUTION);
67 if (attribution && *attribution)
68 {
69 std::string::size_type attrib_start_pos = body.find (attribution);
70 if (attrib_start_pos != std::string::npos)
71 {
72 // the +2 is to trim out the following carriage returns
73 const int attrib_len (::strlen(attribution) + 2);
74 body.erase (attrib_start_pos, attrib_len);
75 }
76 }
77
78 // strip out the signature
79 char * sig_delimiter = 0;
80 // FIXME
81 if (pan_find_signature_delimiter (body->str, &sig_delimiter) != SIG_NONE)
82 {
83 ccc ; g_string_truncate (body, sig_delimiter - body->str);
84
85 pan_g_string_strstrip (body);
86 }
87
88 return body;
89 }
90 #endif
91
92 /**
93 * Check to see if the user is top-posting.
94 */
95 void
check_topposting(unique_strings_t & errors,MessageCheck::Goodness & goodness,const TextMassager & tm,const StringView & body,GMimeMessage * message)96 check_topposting (unique_strings_t & errors,
97 MessageCheck::Goodness & goodness,
98 const TextMassager & tm,
99 const StringView & body,
100 GMimeMessage * message)
101 {
102 // if it's not a reply, then top-posting check is moot
103 if (g_mime_object_get_header ((GMimeObject *) message, "References") == NULL)
104 return;
105
106 bool quoted_found (false);
107 bool original_found_after_quoted (false);
108 StringView v(body), line;
109 while (v.pop_token (line, '\n')) {
110 if (line == "-- ") // signature reached
111 break;
112 if (tm.is_quote_character (g_utf8_get_char (line.str))) // check for quoted
113 quoted_found = true;
114 else if (quoted_found) { // check for non-quoted after quoted
115 line.trim ();
116 original_found_after_quoted = !line.empty();
117 if (original_found_after_quoted)
118 break;
119 }
120 }
121
122 if (quoted_found && !original_found_after_quoted) {
123 goodness.raise_to_warn ();
124 errors.insert (_("Warning: Reply seems to be top-posted."));
125 }
126 }
127
128 /**
129 * Check to see if the signature (if found) is within the McQuary limit of
130 * four lines and 80 columns per line.
131 */
check_signature(unique_strings_t & errors,MessageCheck::Goodness & goodness,const StringView & body)132 void check_signature (unique_strings_t & errors,
133 MessageCheck::Goodness & goodness,
134 const StringView & body)
135 {
136 int sig_point (0);
137 const GNKSA::SigType sig_type (GNKSA::find_signature_delimiter (body, sig_point));
138
139 if (sig_type == GNKSA::SIG_NONE)
140 return;
141
142 if (sig_type == GNKSA::SIG_NONSTANDARD)
143 {
144 goodness.raise_to_warn ();
145 errors.insert (_("Warning: The signature marker should be “-- ”, not “--”."));
146 }
147
148 // how wide and long is the signature?
149 int sig_line_qty (-1);
150 int too_wide_qty (0);
151 StringView line, sig(body);
152 sig.eat_chars (sig_point);
153 while (sig.pop_token (line, '\n')) {
154 ++ sig_line_qty;
155 if (line.len > 80)
156 ++too_wide_qty;
157 }
158
159 if (sig_line_qty == 0)
160 {
161 goodness.raise_to_warn ();
162 errors.insert (_("Warning: Signature prefix with no signature."));
163 }
164 if (sig_line_qty > 4)
165 {
166 goodness.raise_to_warn ();
167 errors.insert (_("Warning: Signature is more than 4 lines long."));
168 }
169 if (too_wide_qty != 0)
170 {
171 goodness.raise_to_warn ();
172 errors.insert (_("Warning: Signature is more than 80 characters wide."));
173 }
174 }
175
176
177 /**
178 * Simple check to see if the body is too wide. Any text after the
179 * signature prefix is ignored in this test.
180 */
check_wide_body(unique_strings_t & errors,MessageCheck::Goodness & goodness,const StringView & body)181 void check_wide_body (unique_strings_t & errors,
182 MessageCheck::Goodness & goodness,
183 const StringView & body)
184 {
185 int too_wide_qty (0);
186
187 StringView v(body), line;
188 while (v.pop_token (line, '\n')) {
189 if (line == "-- ")
190 break;
191 if (line.len > 80)
192 ++too_wide_qty;
193 }
194
195 if (too_wide_qty) {
196 char buf[1024];
197 g_snprintf (buf, sizeof(buf), ngettext(
198 "Warning: %d line is more than 80 characters wide.",
199 "Warning: %d lines are more than 80 characters wide.", too_wide_qty), too_wide_qty);
200 errors.insert (buf);
201 goodness.raise_to_warn ();
202
203 }
204 }
205
206 /**
207 * Check to see if the article appears to be empty.
208 * Any text after the signature prefix is ignored in this test.
209 */
check_empty(unique_strings_t & errors,MessageCheck::Goodness & goodness,const StringView & body)210 void check_empty (unique_strings_t & errors,
211 MessageCheck::Goodness & goodness,
212 const StringView & body)
213 {
214 StringView v(body), line;
215 while (v.pop_token (line, '\n')) {
216 if (line == "-- ") // sig reached;
217 break;
218 line.trim ();
219 if (!line.empty()) // found text
220 return;
221 }
222
223 errors.insert (_("Error: Message is empty."));
224 goodness.raise_to_refuse ();
225 }
226
227 /**
228 * Check to see how much original content is in this message, opposed
229 * to quoted content. Any text after the signature prefix is ignored
230 * in this test.
231 *
232 * (1) count all the lines beginning with the quoted prefix.
233 * (2) count all the nonempty nonsignature lines. These are the orignal lines.
234 * (3) if the ratio of original/quoted is 20% or less, warn.
235 * (4) if the ratio of original/quoted is 0%, warn louder.
236 */
237 void
check_mostly_quoted(unique_strings_t & errors,MessageCheck::Goodness & goodness,const StringView & body)238 check_mostly_quoted (unique_strings_t & errors,
239 MessageCheck::Goodness & goodness,
240 const StringView & body)
241 {
242 int total(0), unquoted(0);
243 StringView v(body), line;
244 while (v.pop_token (line, '\n')) {
245 if (line == "-- ") break; // sig reached
246 line.trim ();
247 if (line.empty())
248 continue;
249 ++total;
250 if (*line.str != '>')
251 ++unquoted;
252 }
253
254 if (total!=0 && ((int)(100.0*unquoted/total)) < 20)
255 {
256 goodness.raise_to_warn ();
257 errors.insert (unquoted==0
258 ? _("Warning: The message is entirely quoted text!")
259 : _("Warning: The message is mostly quoted text."));
260 }
261 }
262
263 /**
264 * Check to see if the article appears to only have quoted text. If this
265 * appears to be the case, we will refuse to post the message.
266 *
267 * (1) Get mutable working copies of the article body and the attribution
268 * string.
269 *
270 * (2) Replace carriage returns in both the calculated attribution string
271 * and a temporary copy of the message body, so that we don't have to
272 * worry whether or not the attribution line's been wrapped.
273 *
274 * (3) Search for an occurance of the attribution string in the body. If
275 * it's found, remove it from the temporary copy of the body so that
276 * it won't affect our line counts.
277 *
278 * (4) Of the remaining body, look for any nonempty lines before the signature
279 * file that don't begin with the quote prefix. If such a line is found,
280 * then the message is considered to not be all quoted text.
281 *
282 */
check_all_quoted(unique_strings_t & errors,MessageCheck::Goodness & goodness,const TextMassager & tm,const StringView & body,const StringView & attribution)283 void check_all_quoted (unique_strings_t & errors,
284 MessageCheck::Goodness & goodness,
285 const TextMassager & tm,
286 const StringView & body,
287 const StringView & attribution)
288 {
289 if (body.empty())
290 return;
291
292 // strip out the attribution, if any
293 std::string s (body.str, body.len);
294
295 if (!attribution.empty()) {
296 std::string::size_type pos = s.find (attribution.str, attribution.len);
297 if (pos != std::string::npos)
298 s.erase (pos, attribution.len+2); // the +2 is to trim out the following carriage returns
299 }
300
301 StringView v(s), line;
302 while (v.pop_token (line, '\n')) {
303 if (line == "-- ") break;
304 line.trim ();
305 if (line.empty()) continue;
306 if (!tm.is_quote_character (g_utf8_get_char (line.str))) return; // found new content
307 }
308
309 errors.insert (_("Error: Message appears to have no new content."));
310 goodness.raise_to_refuse ();
311 }
312
313
check_body(unique_strings_t & errors,MessageCheck::Goodness & goodness,const TextMassager & tm,GMimeMessage * message,const StringView & body,const StringView & attribution)314 void check_body (unique_strings_t & errors,
315 MessageCheck::Goodness & goodness,
316 const TextMassager & tm,
317 GMimeMessage * message,
318 const StringView & body,
319 const StringView & attribution)
320 {
321 check_empty (errors, goodness, body);
322 check_wide_body (errors, goodness, body);
323 check_signature (errors, goodness, body);
324 check_mostly_quoted (errors, goodness, body);
325 check_all_quoted (errors, goodness, tm, body, attribution);
326 check_topposting (errors, goodness, tm, body, message);
327 }
328
329 void
check_followup_to(unique_strings_t & errors,MessageCheck::Goodness & goodness,const quarks_t & groups_our_server_has,const quarks_t & group_names)330 check_followup_to (unique_strings_t & errors,
331 MessageCheck::Goodness & goodness,
332 const quarks_t & groups_our_server_has,
333 const quarks_t & group_names)
334 {
335 const Quark poster ("poster");
336
337 // check to make sure all the groups exist
338 foreach_const (quarks_t, group_names, it) {
339 if (*it == poster)
340 continue;
341 else if (!groups_our_server_has.count (*it)) {
342 goodness.raise_to_warn ();
343 char * tmp = g_strdup_printf (
344 _("Warning: The posting profile’s server doesn’t carry newsgroup\n"
345 "\t“%s”.\n"
346 "\tIf the group name is correct, switch profiles in the “From:”\n"
347 "\tline or edit the profile with “Edit → Manage Posting Profiles”."), it->c_str());
348 errors.insert (tmp);
349 g_free (tmp);
350 }
351 }
352
353 // warn if too many followup-to groups
354 if (group_names.size() > 5u) {
355 errors.insert (_("Warning: Following-Up to too many groups."));
356 goodness.raise_to_warn ();
357 }
358 }
359
check_subject(unique_strings_t & errors,MessageCheck::Goodness & goodness,const StringView & subject)360 void check_subject (unique_strings_t & errors,
361 MessageCheck::Goodness & goodness,
362 const StringView & subject)
363 {
364 if (subject.empty()) {
365 goodness.raise_to_refuse ();
366 errors.insert (_("Error: No Subject specified."));
367 }
368 }
369
check_groups(unique_strings_t & errors,MessageCheck::Goodness & goodness,const quarks_t & groups_our_server_has,const quarks_t & group_names,bool followup_to_set)370 void check_groups (unique_strings_t & errors,
371 MessageCheck::Goodness & goodness,
372 const quarks_t & groups_our_server_has,
373 const quarks_t & group_names,
374 bool followup_to_set)
375 {
376 // make sure all the groups exist and are writable
377 foreach_const (quarks_t, group_names, it)
378 {
379 if (!groups_our_server_has.count (*it))
380 {
381 goodness.raise_to_warn ();
382 char * tmp = g_strdup_printf (
383 _("Warning: The posting profile’s server doesn’t carry newsgroup\n"
384 "\t“%s”.\n"
385 "\tIf the group name is correct, switch profiles in the “From:”\n"
386 "\tline or edit the profile with “Edit → Manage Posting Profiles”."), it->c_str());
387 errors.insert (tmp);
388 g_free (tmp);
389 }
390 #if 0
391 if (data.get_group_permission (*it) == 'n')
392 {
393 goodness.raise_to_warn ();
394 char buf[1024];
395 g_snprintf (buf, sizeof(buf), _("Warning: Group “%s” is read-only."), it->c_str());
396 errors.insert (buf);
397 }
398 #endif
399 }
400
401 if (group_names.size() >= 10u) // refuse if far too many groups
402 {
403 goodness.raise_to_refuse ();
404 errors.insert (_("Error: Posting to a very large number of groups."));
405 }
406 else if (group_names.size() > 5) // warn if too many groups
407 {
408 goodness.raise_to_warn ();
409 errors.insert (_("Warning: Posting to a large number of groups."));
410 }
411
412 // warn if too many groups and no followup-to
413 if (group_names.size()>2u && !followup_to_set)
414 {
415 goodness.raise_to_warn ();
416 errors.insert (_("Warning: Crossposting without setting Followup-To header."));
417 }
418 }
419 }
420
421 void
message_check(const GMimeMessage * message_const,const StringView & attribution,const quarks_t & groups_our_server_has,unique_strings_t & errors,Goodness & goodness,bool binpost)422 MessageCheck :: message_check (const GMimeMessage * message_const,
423 const StringView & attribution,
424 const quarks_t & groups_our_server_has,
425 unique_strings_t & errors,
426 Goodness & goodness,
427 bool binpost)
428 {
429
430 goodness.clear ();
431 errors.clear ();
432
433 // we only use accessors in here, but the GMime API doesn't allow const...
434 GMimeMessage * message (const_cast<GMimeMessage*>(message_const));
435
436 // check the subject...
437 check_subject (errors, goodness, g_mime_message_get_subject (message));
438
439 // check the author...
440 if (GNKSA::check_from (g_mime_object_get_header ((GMimeObject *) message, "From"), true)) {
441 errors.insert (_("Error: Bad email address."));
442 goodness.raise_to_warn ();
443 }
444
445 // check the body...
446 TextMassager tm;
447 gboolean is_html;
448 char * body = pan_g_mime_message_get_body (message, &is_html);
449 if (is_html && !binpost) {
450 errors.insert (_("Warning: Most newsgroups frown upon HTML posts."));
451 goodness.raise_to_warn ();
452 }
453
454 if (!binpost)
455 check_body (errors, goodness, tm, message, body, attribution);
456 g_free (body);
457
458 // check the optional followup-to...
459 bool followup_to_set (false);
460 const char * cpch = g_mime_object_get_header ((GMimeObject *) message, "Followup-To");
461 if (!binpost)
462 {
463 if (cpch && *cpch) {
464 quarks_t groups;
465 get_nntp_rcpts (cpch, groups);
466 followup_to_set = !groups.empty();
467 check_followup_to (errors, goodness, groups_our_server_has, groups);
468 }
469 } else
470 followup_to_set = true;
471
472 // check the groups...
473 size_t group_qty (0);
474 cpch = g_mime_object_get_header ((GMimeObject *) message, "Newsgroups");
475 if (cpch && *cpch) {
476 quarks_t groups;
477 get_nntp_rcpts (cpch, groups);
478 check_groups (errors, goodness, groups_our_server_has, groups, followup_to_set);
479 group_qty = groups.size ();
480 }
481
482 // one last error check
483 #ifdef HAVE_GMIME_30
484 InternetAddressList * list (g_mime_message_get_addresses (message, GMIME_ADDRESS_TYPE_TO));
485 #else
486 InternetAddressList * list (g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO));
487 #endif
488 const int n_to (internet_address_list_length (list));
489 if (!group_qty && !n_to) {
490 errors.insert (_("Error: No Recipients."));
491 goodness.raise_to_refuse ();
492 }
493 }
494