Logo Search packages:      
Sourcecode: pan version File versions  Download package

message-check.c

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Pan - A Newsreader for Gtk+
 * Copyright (C) 2002  Charles Kerr <charles@rebelbase.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <config.h>

#include <ctype.h>
#include <string.h>

#include <glib.h>

#include <gmime/gmime-charset.h>
#include <gmime/gmime-utils.h>
#include <gmime/internet-address.h>

#include <pan/base/article.h>
#include <pan/base/base-prefs.h>
#include <pan/base/gnksa.h>
#include <pan/base/message-check.h>
#include <pan/base/pan-i18n.h>
#include <pan/base/pan-glib-extensions.h>
#include <pan/base/text-massager.h>

/***
****  PRIVATE UTILITIES
***/


/* Find and return a list of NNTP groups to send to */
static GPtrArray*
get_nntp_rcpts (const gchar *to)
{
      gint line_len = 0;
      const gchar * line_begin = NULL;
      const gchar * march = to;
      GPtrArray * a = g_ptr_array_new ();

      /* march through the tokens & save the nonempty ones */
      while (get_next_token_run (march, ',', &march, &line_begin, &line_len))
            g_ptr_array_add (a, g_strndup (line_begin, line_len));

      return a;
}


/***
****  OUTGOING MESSAGE CHECKS
***/

static char*
strip_attribution_and_signature (const char * body_in, GMimeMessage * message)
{
      const char * attribution;
      char * sig_delimiter;
      GString * body;

      /* sanity clause */
      g_return_val_if_fail (body_in!=NULL, NULL);
      g_return_val_if_fail (message!=NULL, NULL);

      /* initialize the gstring */
      body = g_string_new (body_in);
      body_in = NULL;

      /* strip out the attribution */
      attribution = g_mime_message_get_header (message, PAN_ATTRIBUTION);
      if (is_nonempty_string (attribution))
      {
            const char * attribution_start_pos = strstr (body->str, attribution);
            if (attribution_start_pos != NULL)
            {
                  const int attribution_start_index = attribution_start_pos - body->str;
                  const int attribution_len = strlen(attribution) + 2; /* +2: trim out the following carriage returns */
                  g_string_erase (body, attribution_start_index, attribution_len);
            }
      }

      /* strip out the signature */
      sig_delimiter = NULL;
      if (pan_find_signature_delimiter (body->str, &sig_delimiter) != SIG_NONE) {
            g_string_truncate (body, sig_delimiter - body->str);
            pan_g_string_strstrip (body);
      }

      return g_string_free (body, FALSE);
}

/**
 * Check to see if the user is top-posting.
 */
static void
check_topposting (const TextMassager   * tm,
                  GPtrArray            * setme_errors,
                  GoodnessLevel        * setme_goodness,
                  const char           * body,
              GMimeMessage         * message)
{
      char * freeme = NULL;
      int line_len;
      const char * march;
      const char * line_begin;
      gboolean original_found_after_quoted = FALSE;
      gboolean quoted_found = FALSE;

      /* sanity clause */
      g_return_if_fail (tm!=NULL);
      g_return_if_fail (setme_errors!=NULL);
      g_return_if_fail (setme_goodness!=NULL);
      g_return_if_fail (body!=NULL);
      g_return_if_fail (message!=NULL);

      /* if it's not a reply, then top-posting check is moot */
      if (g_mime_message_get_header (message, HEADER_REFERENCES) == NULL)
            return;

      /* walk through the body, looking for an original line after a quoted one */
      march = body = freeme = strip_attribution_and_signature (body, message);
      while ((get_next_token_run (march, '\n', &march, &line_begin, &line_len))) {
            if (text_massager_is_quote_char (tm, (guchar)line_begin[0])) /* check for quoted */
                  quoted_found = TRUE;
            else if (quoted_found) /* check for non-quoted after quoted */
                  original_found_after_quoted = TRUE;
      }

      /* did they put anything after the quote? */
      if (quoted_found && !original_found_after_quoted) {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: Reply seems to be top-posted.")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }

      /* cleanup */
      g_free (freeme);
}


/**
 * Check to see if the signature (if found) is within the McQuary limit of
 * four lines and 80 columns per line.
 */
static void
check_signature (GPtrArray      * setme_errors,
                 GoodnessLevel  * setme_goodness,
                 const char     * body)
{
      gint too_wide_qty = 0;
      gint sig_line_qty = -1;
      gint line_len = 0;
      const char * pch = NULL;
      const char * line_begin = NULL;
      char * sig_point = NULL;
      SigType sig_type;

      g_return_if_fail (setme_errors!=NULL);
      g_return_if_fail (setme_goodness!=NULL);
      g_return_if_fail (is_nonempty_string(body));

      sig_type = pan_find_signature_delimiter (body, &sig_point);

      if (sig_type == SIG_NONSTANDARD)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: The signature marker should be \"-- \", not \"--\".")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }

      pch = sig_point;
      while (get_next_token_run (pch, '\n', &pch, &line_begin, &line_len))
      {
            ++sig_line_qty;

            if (line_len>80)
                  ++too_wide_qty;
      }

      if (sig_line_qty == 0)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: Signature prefix with no signature.")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }

      else if (sig_line_qty > 4)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: Signature is more than 4 lines long")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }

      if (too_wide_qty != 0)
      {
            g_ptr_array_add (setme_errors, g_strdup(_("WARNING: Signature is more than 80 characters wide.")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }
}


/**
 * Simple check to see if the body is too wide.  Any text after the
 * signature prefix is ignored in this test.
 */
static void
check_wide_body (GPtrArray      * setme_errors,
                 GoodnessLevel  * setme_goodness,
                 const gchar    * body)
{
      gint line_len = 0;
      const gchar * line_begin = NULL;
      int too_wide_qty = 0;

      g_return_if_fail (is_nonempty_string(body));

      while (get_next_token_run (body, '\n', &body, &line_begin, &line_len))
      {
            if (line_len==3 && !strncmp(line_begin,"-- ",3))
                  break;
            if (line_len>80)
                  ++too_wide_qty;
      }

      if (too_wide_qty != 0)
      {
            g_ptr_array_add (setme_errors, g_strdup_printf(
                  _("WARNING: %d lines are more than 80 characters wide."),
                  too_wide_qty));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }
}

/**
 * Check to see if the article appears to be empty.  Any text after the
 * signature prefix is ignored in this test.
 */
static void
check_empty (GPtrArray      * setme_errors,
             GoodnessLevel  * setme_goodness,
             const gchar    * body)
{
      if (is_nonempty_string(body))
      {
            gint line_len = 0;
            const gchar * line_begin = NULL;

            while (get_next_token_run (body, '\n', &body, &line_begin, &line_len))
            {
                  const gchar * pch = line_begin;
                  const gchar * end = line_begin + line_len;

                  if (line_len==3 && !strncmp(line_begin,"-- ",3))
                        break;

                  while (pch!=end && isspace((guchar)*pch))
                        ++pch;
                  if (pch!=end) /* found text */
                        return;
            }
      }

      g_ptr_array_add (setme_errors, g_strdup(
            _("ERROR: Message is empty.")));
      *setme_goodness = MAX (*setme_goodness, REFUSE);
}

/**
 * Check to see how much original content is in this message, opposed
 * to quoted content.  Any text after the signature prefix is ignored
 * in this test.
 *
 * (1) count all the lines beginning with the quoted prefix.
 * (2) count all the nonempty nonsignature lines.  These are the orignal lines.
 * (3) if the ratio of original/quoted is 20% or less, warn.
 * (4) if the ratio of original/quoted is 0%, warn louder.
 */
static void
check_mostly_quoted (GPtrArray      * setme_errors,
                     GoodnessLevel  * setme_goodness,
                     const gchar    * body)
{
      gint line_len = 0;
      const gchar * line_begin = NULL;
      int unquoted = 0;
      int total = 0;

      g_return_if_fail (is_nonempty_string(body));

      while (get_next_token_run (body, '\n', &body, &line_begin, &line_len))
      {
            const gchar * pch = line_begin;
            const gchar * end = line_begin + line_len;

            /* see if sig reached */
            if (line_len==3 && !strncmp(line_begin,"-- ",3))
                  break;

            while (pch!=end && isspace((guchar)*pch))
                  ++pch;
            if (pch==end) /* empty line */
                  continue;

            /* nonempty line */
            ++total;
            if (*pch != '>')
                  ++unquoted;
      }

      if (total!=0 && ((int)(100.0*unquoted/total)) < 20)
      {
            char * pch = g_strdup (unquoted==0
                  ?  _("WARNING: The message is entirely quoted text!")
                  :  _("WARNING: The message is mostly quoted text."));
            g_ptr_array_add (setme_errors, pch);
            *setme_goodness = MAX (*setme_goodness, WARN);
      }
}

/**
 * Check to see if the article appears to only have quoted text.  If this
 * appears to be the case, we will refuse to post the message.
 *
 * (1) Get mutable working copies of the article body and the attribution
 *     string.
 *
 * (2) Replace carriage returns in both the calculated attribution string
 *     and a temporary copy of the message body, so that we don't have to
 *     worry whether or not the attribution line's been wrapped.
 * 
 * (3) Search for an occurance of the attribution string in the body.  If
 *     it's found, remove it from the temporary copy of the body so that
 *     it won't affect our line counts.
 *
 * (4) Of the remaining body, look for any nonempty lines before the signature
 *     file that don't begin with the quote prefix.  If such a line is found,
 *     then the message is considered to not be all quoted text.
 *
 */
static void
check_all_quoted (const TextMassager   * tm,
                  GPtrArray            * setme_errors,
                  GoodnessLevel        * setme_goodness,
                const char           * body,
              GMimeMessage         * message)
{
      int line_len;
      char * freeme;
      const char * line_begin;
      const char * march;

      g_return_if_fail (is_nonempty_string(body));

      march = body = freeme = strip_attribution_and_signature (body, message);
      while (get_next_token_run (march, '\n', &march, &line_begin, &line_len))
      {
            const char * march = line_begin;
            const char * end = line_begin + line_len;
            while (march!=end && isspace((guchar)*march))
                  ++march;
            if (march==end) /* blank line */
                  continue;
            if (!text_massager_is_quote_char (tm, (guchar)*march)) { /* found new content */
                  g_free (freeme);
                  return;
            }
      }

      g_ptr_array_add (setme_errors, g_strdup(
            _("ERROR: Message appears to have no new content.")));
      *setme_goodness = MAX (*setme_goodness, REFUSE);

      /* cleanup */
      g_free (freeme);
}

static void
check_body (const TextMassager    * tm,
            GPtrArray             * setme_errors,
          GoodnessLevel         * setme_goodness,
          GMimeMessage          * message,
          const gchar           * body)
{
      gboolean posting = is_nonempty_string (g_mime_message_get_header (message, HEADER_NEWSGROUPS));

      g_return_if_fail (setme_goodness != NULL);
      g_return_if_fail (setme_errors != NULL);
      g_return_if_fail (message != NULL);

      if (posting)
      {
            check_empty (setme_errors, setme_goodness, body);
            check_wide_body (setme_errors, setme_goodness, body);
            check_signature (setme_errors, setme_goodness, body);
            check_mostly_quoted (setme_errors, setme_goodness, body);
            check_all_quoted (tm, setme_errors, setme_goodness, body, message);
            check_topposting (tm, setme_errors, setme_goodness, body, message);
      }
}



static void
check_followup_to (GPtrArray        * setme_errors,
               GoodnessLevel    * setme_goodness,
               Server           * server,
               const GPtrArray  * group_names)
{
      const PString poster = pstring_shallow ("poster", 6);
      int i;

      g_return_if_fail (setme_errors != NULL);
      g_return_if_fail (setme_goodness != NULL);
      g_return_if_fail (server != NULL);
      g_return_if_fail (group_names != NULL);

      /* check to make sure all the groups exist */
      for (i=0; i<group_names->len; ++i)
      {
            const PString name = pstring_shallow (g_ptr_array_index (group_names, i), -1);

            if (!pstring_equal (&name, &poster)
                  && server_get_named_group(server,&name)==NULL)
            {
                  g_ptr_array_add (setme_errors, g_strdup_printf(
                        _("WARNING: Unknown group: \"%*.*s\"."), name.len, name.len, name.str));
                  *setme_goodness = MAX (*setme_goodness, WARN);
            }
      }

      /* warn if too many followup-to groups */
      if (group_names->len > 5)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: Following-Up too many groups.")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }
}

static void
check_subject (GPtrArray * setme_errors,
             GoodnessLevel * setme_goodness,
             const gchar * subject)
{
      g_assert (setme_errors!=NULL);
      g_assert (setme_goodness!=NULL);

      if (!is_nonempty_string (subject))
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("ERROR: No subject specified.")));
            *setme_goodness = MAX (*setme_goodness, REFUSE);
      }
}

static void
check_groups (GPtrArray          * setme_errors,
            GoodnessLevel      * setme_goodness,
            Server             * server,
            const GPtrArray    * group_names,
            gboolean             followup_to_set)
{
      int i;

      g_return_if_fail (setme_errors!=NULL);
      g_return_if_fail (setme_goodness!=NULL);
      g_return_if_fail (server_is_valid (server));
      g_return_if_fail (group_names!=NULL);

      /* check to make sure all the groups exist */
      for (i=0; i<group_names->len; ++i)
      {
            const PString name = pstring_shallow (g_ptr_array_index (group_names, i), -1);
            Group * g = server_get_named_group (server, &name);

            if (g == NULL)
            {
                  g_ptr_array_add (setme_errors, g_strdup_printf(
                        _("WARNING: Unknown group: \"%*.*s\"."), name.len, name.len, name.str));
                  *setme_goodness = MAX (*setme_goodness, WARN);
            }
            else if (group_is_read_only (g))
            {
                  g_ptr_array_add (setme_errors, g_strdup_printf(
                        _("WARNING: Group \"%*.*s\" is read-only."), name.len, name.len, name.str));
                  *setme_goodness = MAX (*setme_goodness, WARN);
            }
      }

      /* refuse if far too many groups */
      if (group_names->len > 12)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("ERROR: Posting to a very large number of groups.")));
            *setme_goodness = MAX (*setme_goodness, REFUSE);
      }
      /* warn if too many groups */
      else if (group_names->len > 5)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: Posting to a large number of groups.")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }


      /* warn if too many groups and no followup-to */
      if (group_names->len>2 && !followup_to_set)
      {
            g_ptr_array_add (setme_errors, g_strdup(
                  _("WARNING: Crossposting without setting Followup-To.")));
            *setme_goodness = MAX (*setme_goodness, WARN);
      }
}

/**
 * @return the number of recipients
 */
static int
check_mail_addresses (GPtrArray      * setme_errors,
                  GoodnessLevel  * setme_goodness,
                  const char     * addresses_str)
{
      guint address_count = 0;
      InternetAddressList * addresses;
      InternetAddressList * march;

      march = addresses = internet_address_parse_string (addresses_str);
      while (march)
      {
            InternetAddress * address = march->address;
            char * from = internet_address_to_string (address, TRUE);
            int val = gnksa_check_from (from, TRUE);

            if (val != GNKSA_OK)
            {
                  *setme_goodness = MAX (*setme_goodness, REFUSE);

                  g_ptr_array_add (setme_errors, g_strdup_printf (
                        _("ERROR: invalid address \"%s\""), from));
            }

            ++address_count;
            march = march->next;

            /* cleanup */
            g_free (from);
      }

      /* cleanup */
      internet_address_list_destroy (addresses);
      return address_count;
}


void
message_check (GMimeMessage        * message,
               Server              * server,
               GPtrArray           * appendme_errors,
               GoodnessLevel       * setme_goodness)
{
      gchar * tmp;
      int mail_qty;
      int group_qty;
      int author_qty;
      const gchar * cpch;
      gboolean is_html;
      gboolean followup_to_set;
      GoodnessLevel goodness = OKAY;
      TextMassager * tm_default_values;

      /* sanity check */
      g_return_if_fail (GMIME_IS_MESSAGE(message));
      g_return_if_fail (appendme_errors!=NULL);
      g_return_if_fail (setme_goodness!=NULL);

      /* check the subject... */
      check_subject (appendme_errors, &goodness, g_mime_message_get_subject(message));

      /* check the body... */
      tm_default_values = text_massager_new ();
      tmp = g_mime_message_get_body (message, TRUE, &is_html);
      check_body (tm_default_values, appendme_errors, &goodness, message, tmp);
      g_free (tmp);
      text_massager_free (tm_default_values);

      /* check the optional followup-to... */
      followup_to_set = FALSE;
      cpch = g_mime_message_get_header (message, HEADER_FOLLOWUP_TO);
      if (is_nonempty_string (cpch))
      {
            GPtrArray * groups = get_nntp_rcpts (cpch);

            followup_to_set = groups->len > 0;
            check_followup_to (appendme_errors,
                               &goodness,
                               server,
                               groups);

            pan_g_ptr_array_foreach (groups, (GFunc)g_free, NULL);
            g_ptr_array_free (groups, TRUE);
      }

      /* check the groups... */
      group_qty = 0;
      cpch = g_mime_message_get_header (message, HEADER_NEWSGROUPS);
      if (is_nonempty_string(cpch))
      {
            GPtrArray * groups = get_nntp_rcpts (cpch);

            group_qty = groups->len;
            check_groups (appendme_errors,
                          &goodness,
                          server,
                          groups,
                          followup_to_set);

            pan_g_ptr_array_foreach (groups, (GFunc)g_free, NULL);
            g_ptr_array_free (groups, TRUE);
      }

      /* check the author... */
      cpch = g_mime_message_get_sender (message);
      author_qty = check_mail_addresses (appendme_errors, &goodness, cpch);
      if (author_qty!=1) {
            /*
             * This is kind of silly: the From is controlled through a 
             * GtkOptionMenu, so we know there's always one selected. 
             * So, the only way to get here is a bug in our code.
             */

            g_ptr_array_add (appendme_errors, g_strdup(
                  _("ERROR: Must have one author in From: line")));
            goodness = MAX (goodness, REFUSE);
      }

      /* check the email recipients... */
      cpch = g_mime_message_get_header (message, HEADER_TO);
      mail_qty = check_mail_addresses (appendme_errors, &goodness, cpch);

      /* one last error check */
      if (!group_qty && !mail_qty) {
            g_ptr_array_add (appendme_errors, g_strdup(
                  _("ERROR: No recipients specified.")));
            goodness = MAX (goodness, REFUSE);
      }

      *setme_goodness = goodness;
}

Generated by  Doxygen 1.6.0   Back to index