/*
 * WMMail - Window Maker Mail
 *
 * Copyright (c) 1996, 1997, 1998  Per Liden
 * Copyright (c) 1997, 1998  Bryan Chan
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * imap.c: functions to handle IMAP4 mailboxes
 *
 * $Id: imap.c,v 1.1 2000/07/02 20:37:58 bryan.chan Exp $
 *
 */

#include <stdio.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <proplist.h>
#include "wmmail.h"
#include "wmutil.h"
#include "net.h"

#ifdef IMAP_SUPPORT

/*
 * Supported key(s) in the Options dictionary:
 *
 *  Hostname
 *  Port (optional)
 *  Username
 *  Password
 *  Folder (optional)
 *
 * See mbox.c for information on states and transitions.
 *
 * For a local mailbox, errors encountered during an update usually
 * cause the program to give up monitoring the mailbox ("ignored").
 *
 * For remote mailboxes, network errors are ignored. Query will be
 * re-tried at the next update. Other types of errors (such as invalid
 * options) still cause the program to give up.
 *
 */

/* internal data structure */
typedef struct {
  char *folder_name;
  int socket;
  int cmd_seq;
} Connection;

/* internal functions */
static Connection *init_connection(Mailbox *);
static int         count_mail(Connection *, int *, int *);
static int         imap_ok(char *, int);
static void        close_connection(Connection *);


#define MAX_STRING_SIZE  1024

#define GET_ENTRY(x,y,z) { proplist_t sub_key = PLMakeString(z); \
                           y = PLGetDictionaryEntry(x, sub_key); \
                           PLRelease(sub_key); }

int IMAP_check(Mailbox *mailbox, int *beep, int *redraw, int *run)
{
  Connection *connection;
  int         prev_status,
              prev_new_mail_count;

  connection = init_connection(mailbox);
  mailbox->last_update = time(NULL);

  if (connection == NULL)
    return False;
  else if (connection == (Connection *) -1)
    return True;
  else
  {
    prev_status = mailbox->status;
    prev_new_mail_count = mailbox->new_mail_count;

    if (count_mail(connection,
                   &mailbox->total_mail_count, 
                   &mailbox->new_mail_count))
    {
      close_connection(connection);

      if (mailbox->total_mail_count == 0)
      {
        /* there is no mail in the mailbox */
        mailbox->status = NO_MAIL;
      }
      else if (mailbox->new_mail_count == 0)
      {
        /* there are no new mails */
        mailbox->status = OLD_MAIL;
      }
      else if (mailbox->new_mail_count > prev_new_mail_count)
      {
        /* new mails have arrived! */
        if (mailbox->status == NEW_MAIL && always_new_mail_exec)
          *run |= True;
        else if (mailbox->status == NEW_MAIL)
          *run |= False;
        else
          *run |= True;

        *beep = True;

        mailbox->status = NEW_MAIL;
      }
      /* else no change */
    }
    else
    {
      croak("cannot count messages in mailbox \"%s\"; ignored", mailbox->name);
      close_connection(connection);
      return False;
    }

    *redraw |= (prev_status != mailbox->status);

    return True;
  }
}


/* creates a connection to the IMAP server and logs in
 *
 * returns:  -1                     if recoverable error encountered
 *           NULL                   if unrecoverable error encountered
 *           pointer to Connection  if successfully initiated
 */
static Connection *init_connection(Mailbox *mailbox)
{
  Connection         *connection;
  struct sockaddr_in  remote_addr;
  struct hostent     *host;
  char                buf[MAX_STRING_SIZE];
  int                 sockfd,
                      port_no = -1;
  proplist_t          hostname,
                      port,
                      username,
                      password,
                      folder;

  GET_ENTRY(mailbox->options, hostname, "Hostname");
  GET_ENTRY(mailbox->options, port, "Port");
  GET_ENTRY(mailbox->options, username, "Username");
  GET_ENTRY(mailbox->options, password, "Password");
  GET_ENTRY(mailbox->options, folder, "Folder");

  if (!username || !password || !hostname)
  {
    croak("mailbox \"%s\" missing one of the options \"Username\", "
          "\"Password\", and \"Hostname\"; ignored", mailbox->name);
    return NULL;
  }
  else if ( !PLIsString(hostname) ||
            !PLIsString(username) ||
            !PLIsString(password) ||
            (folder && !PLIsString(folder)) ||
            (port && (!PLIsString(port) ||
                      sscanf(PLGetString(port), "%i", &port_no) != 1)) )
  {
    croak("mailbox \"%s\" has invalid options; ignored", mailbox->name);
    return (Connection *) NULL;
  }

  if (( host = gethostbyname(PLGetString(hostname)) ) == NULL)
  {
    croak("gethostbyname() failed for mailbox \"%s\"; continuing",
          mailbox->name);
    return (Connection *) -1;
  }

  if (( sockfd = socket(AF_INET, SOCK_STREAM, 0) ) == -1)
  {
    croak("socket() failed for mailbox\"%s\"; continuing", mailbox->name);
    return (Connection *) -1;
  }

  remote_addr.sin_family = AF_INET;
  remote_addr.sin_port   = htons(port_no == -1 ? 143 : port_no);
  remote_addr.sin_addr   = *((struct in_addr *) host->h_addr);
  bzero(&remote_addr.sin_zero, 8);

  if (connect(sockfd, (struct sockaddr *) &remote_addr,
              sizeof(struct sockaddr)) == -1)
  {
    croak("connect() failed for mailbox\"%s\"; continuing", mailbox->name);
    close(sockfd);
    return (Connection *) -1;
  }

  /* are we welcome? */
  socket_read(sockfd, buf, MAX_STRING_SIZE);
  if (!imap_ok(buf, -1))
  {
    /* give up if not; we don't want to force open a door that won't */
    croak("no service from remote server for mailbox \"%s\"; ignored",
          mailbox->name);
    close(sockfd);
    return NULL;
  }

  sprintf(buf, "A000 LOGIN %s %s\r\n", PLGetString(username),
          PLGetString(password));
  socket_write(sockfd, buf);

  socket_read(sockfd, buf, MAX_STRING_SIZE);
  if (imap_ok(buf, 0))
  {
    connection = wmalloc(sizeof(Connection));
    connection->folder_name = folder ? PLGetString(folder) : "INBOX";
    connection->socket = sockfd;
    connection->cmd_seq = 1;  /* A000 is already issued, above */

    return connection;
  }
  else
  {
    /* FIXME: remove this debug message before release! */
    croak("%s", buf);
    /* if we can't login, then don't try any more */
    croak("login failed for mailbox \"%s\"; ignored", mailbox->name);
    close(sockfd);
    return NULL;
  }
}


/* FIXME: socket errors are transparent! */
static int count_mail(connection, total_mail_count, new_mail_count)
  Connection *connection;
  int        *total_mail_count,
             *new_mail_count;
{
  char  buf[MAX_STRING_SIZE + 1];
  char  cmd[MAX_STRING_SIZE + 1];

  buf[MAX_STRING_SIZE] = 0;  /* fix buffer overflow */
  cmd[MAX_STRING_SIZE] = 0;

  sprintf(cmd, "A%03i SELECT %s\r\n",
          connection->cmd_seq++,
          connection->folder_name);

  if (socket_write(connection->socket, cmd))
    do
    {
      if (!socket_read(connection->socket, buf, MAX_STRING_SIZE))
        break;

      if (strstr(buf, "EXISTS"))
        *total_mail_count = atoi(strstr(buf, "*") + 1);
    }
    while (strncmp(buf, cmd, 4));

  sprintf(cmd, "A%03i CHECK\r\n", connection->cmd_seq++);
  if (socket_write(connection->socket, cmd))
    do
      if (!socket_read(connection->socket, buf, MAX_STRING_SIZE))
        break;
    while (strncmp(buf, cmd, 4));

  *new_mail_count = 0;

  sprintf(cmd, "A%03i SEARCH UNSEEN\r\n", connection->cmd_seq++);
  if (socket_write(connection->socket, cmd))
  {
    socket_read(connection->socket, buf, MAX_STRING_SIZE);
    while (strncmp(buf, cmd, 4))
    {
      char *c;

      /* From RFC1730:  
       *
       * " SEARCH Response
       *
       *   Data:  zero or more numbers
       *
       *   .... The number(s) refer to those messages that match the
       *   search criteria. For SEARCH, these are message sequence
       *   numbers;.... Each number is delimited by a space.
       *
       *   Example:  S: * SEARCH 2 3 6 10 "
       *
       */

      /* chop trailing whitespace to avoid counting errors */
      for (c = &buf[strlen(buf) - 1]; *c == ' '; c--)
        *c = '\0';

      for (c = &buf[7]; *c != '\0'; c++)
        if (*c == ' ')
          (*new_mail_count)++;

      /* server closed connection? */
      if (!socket_read(connection->socket, buf, MAX_STRING_SIZE))
        break;
    }
  }

  return True;
}


/* verify that the return string from the server contains "OK";
 * we should probably make use of this function more frequently
 */
static int imap_ok(char *buf, int cmd_seq)
{
  /* make a slightly larger buffer just in case;
   * maybe we should just assert(cmd_seq <= 999)
   */
  char s[10];

  if (!buf)
    return False;

  /* assume untagged response if cmd_seq is negative */
  if (cmd_seq < 0)
    sprintf(s, "* OK");
  else
    sprintf(s, "A%03i OK", cmd_seq);

  if (strncasecmp(buf, s, strlen(s)))
    return False;
  else
    return True;
}


static void close_connection(Connection *connection)
{
  char buf[MAX_STRING_SIZE];

  sprintf(buf, "A%03i LOGOUT\r\n", connection->cmd_seq);
  socket_write(connection->socket, buf);
  socket_read(connection->socket, buf, MAX_STRING_SIZE);
  close(connection->socket);
  wfree(connection);
}

#endif


syntax highlighted by Code2HTML, v. 0.9.1