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