/*! * \file 2hermes/imapupload.c * * \brief Upload a list of mailboxes to an IMAP server. * * This program attempts to deal gracefully with name clashes. * * The error handling is very basic, and little attention has been * paid to memory leaks. * * \author Tony Finch * * $Cambridge: hermes/src/2hermes/imapupload.c,v 1.69 2005/06/13 14:37:56 fanf2 Exp $ */ static const char camid[] = "$Cambridge: hermes/src/2hermes/imapupload.c,v 1.69 2005/06/13 14:37:56 fanf2 Exp $"; #include #include #include #include #include #include #include #include #include #include #include "c-client.h" #include "err.h" #include "list.h" #include "stdbool.h" static long appendmessage(MAILSTREAM *, void *, char **, char **, STRING **); /**********************************************************************/ /*! * \name global state variables * \{ */ static bool critical; /*!< c-client is in critical mode */ static int debuglevel; /*!< how much debugging output? */ static const char *username; /*!< the user's name */ static const char *password; /*!< the user's password */ static const char *servername; /*!< IMAP server name in curlies */ static struct list *serverlist; /*!< list of mailboxes on server */ static struct list *clientlist; /*!< list of local mailboxes */ static MAILSTREAM *serverstream; /*!< IMAP connection to server */ static MAILSTREAM *clientstream; /*!< local mailbox accessor */ static char *msgdate; /*!< message metadata to be freed */ static char *msgflags; /*!< message metadata to be freed */ static int msgnum; /*!< index of message to append */ static STRING msgstr; /*!< message data to append */ static char buffer[1024]; /*!< general-purpose buffer */ /*! * \} */ /**********************************************************************/ /*! * \name Misc sugar * \{ */ static void * xalloc(size_t size) { char *ptr = malloc(size); if(ptr == NULL) err(1, "malloc"); return(ptr); } static char * xstrdup(const char *old) { char *new = strdup(old); if(new == NULL) err(1, "malloc"); return(new); } static char * xasprintf(const char *fmt, ...) { va_list ap; size_t size; char *ptr, c; va_start(ap, fmt); /* work around Solaris POSIX compliance bug */ size = vsnprintf(&c, 1, fmt, ap); va_end(ap); va_start(ap, fmt); ptr = xalloc(size + 1); vsnprintf(ptr, size + 1, fmt, ap); va_end(ap); return(ptr); } static bool fgetline(FILE *stream, char *buf, size_t size) { char *ret; int len; ret = fgets(buf, size, stream); if(ret == NULL) { if(feof(stream)) return(false); else err(1, "error reading input"); } len = strlen(buf); if(len == 0 || buf[len - 1] != '\n') errx(1, "error reading input: missing newline"); buf[len - 1] = '\0'; return(true); } static bool getline(char *buf, size_t size) { return(fgetline(stdin, buf, size)); } static void xfgetline(FILE *stream, char *buf, size_t size) { if(fgetline(stream, buf, size) == false) errx(1, "error reading input: unexpected end-of-file"); } static void xgetline(char *buf, size_t size) { xfgetline(stdin, buf, size); } /*! * \} */ /**********************************************************************/ /*! * \name lists of mailboxes * \{ */ list_types(list, elem, link); struct elem { struct link link; const char *old; /*!< original mailbox name */ const char *tmp; /*!< temporary mailbox name */ const char *new; /*!< altered name to avoid clashes */ int attr; /*!< server mailbox flags */ }; list_functions(list, struct list, struct elem, link); static struct list * list_create(void) { struct list *list; list = xalloc(sizeof(*list)); list_init(list); return(list); } static struct elem * list_create_elem(const char *str) { struct elem *elem; elem = xalloc(sizeof(*elem)); elem->old = xstrdup(str); elem->tmp = NULL; elem->new = NULL; elem->attr = 0; return(elem); } static int list_elem_cmp_new(struct elem *e1, struct elem *e2) { return(strcmp(e1->new, e2->new)); } static int list_elem_cmp_str(struct elem *e1, const void *ptr) { return(strcmp(e1->old, ptr)); } /*! * \} */ /**********************************************************************/ /*! * \name Main program etc. * \{ */ /*! * \brief Explain command line options. */ static void usage(void) { fprintf(stderr, "usage: %s [-acdfpqr] [-u uid] \n" " -a Allow absolute pathnames\n" " -c Confirm on command line not interactively\n" " -d Increase debugging level\n" " -f Do local mailbox name fix-ups\n" " -n Do not do the actual upload\n" " -p Read password from stdin\n" " (default is /dev/tty)\n" " -q Be quiet\n" " -r Rename local mailboxes to avoid clashes\n" " (default is to append to existing mailbox)\n" " -u Optional username\n" "The input to imapuload is a list of email folders " "to upload, one per line.\n", getprogname()); exit(1); } /*! * \brief The main program. */ int main(int argc, char *argv[]) { int c; bool allowabs, confirmed, fixup; bool mailprefix, noaction; bool pipepasswd, quiet, renameclashes; struct elem *elem, *next, *srv; const char *s; FILE *subs_file; /* general initialization */ setprogname(argv[0]); serverlist = list_create(); clientlist = list_create(); allowabs = confirmed = fixup = false; mailprefix = noaction = false; pipepasswd = quiet = renameclashes = false; /* c-client initialization */ # include "linkage.c" /* don't be so stupid */ mail_parameters(NIL, SET_RSHTIMEOUT, 0); /* argument parsing */ while((c = getopt(argc, argv, "acdfmnpqru:")) != -1) switch(c) { case('a'): /* Allow absolute pathnames */ allowabs = true; break; case('c'): /* Don't check with the user */ confirmed = true; break; case('d'): /* Debugging */ debuglevel++; err_set_debug(debuglevel); break; case('f'): /* Local mailbox name fix-ups */ fixup = true; break; case('m'): /* Prefix remote folder names with mail/ */ mailprefix = true; break; case('n'): /* Do nothing */ noaction = true; break; case('p'): /* Read password from stdin */ pipepasswd = true; break; case('q'): /* Be quiet */ quiet = true; break; case('r'): /* Rename clashing mailboxes */ renameclashes = true; break; case('u'): /* Non-default username */ username = optarg; break; default: usage(); } argc -= optind; argv += optind; if(argc != 1) usage(); servername = xasprintf("{%s}", argv[0]); debug(1, "servername %s", servername); if(pipepasswd) { xgetline(buffer, sizeof(buffer)); password = xstrdup(buffer); } else { password = getpass("Password: "); } if(username == NULL) { uid_t uid = getuid(); struct passwd *pw = getpwuid(uid); if(pw != NULL) username = xstrdup(pw->pw_name); } if(username == NULL) { printf("Username: "); xgetline(buffer, sizeof(buffer)); username = xstrdup(buffer); } /* read list of local mailboxes to upload */ while(getline(buffer, sizeof(buffer))) { elem = list_create_elem(buffer); list_insert_last(clientlist, elem); debug(1, "client filename: %s", elem->old); if(!fixup) { elem->new = xasprintf("%s%s", servername, elem->old + strspn(elem->old, "./")); continue; } /* rename user's inbox */ if(strncmp(elem->old, "/var/mail/", 10) == 0 || strncmp(elem->old, "/var/spool/mail/", 16) == 0) { elem->new = elem->old + (elem->old[5] == 'm' ? 10 : 16); if(strcmp(elem->new, username) == 0) { elem->new = "INBOX"; } else if(!allowabs) { warnx("ignoring mailbox %s: " "doesn't match username %s", elem->old, username); list_remove(elem); continue; } else { elem->new = xasprintf("inbox-%s", elem->new); } /* mailboxes outside the user's home */ } else if(elem->old[0] == '/') { if(!allowabs) { warnx("ignoring mailbox %s: " "absolute pathnames not permitted", elem->old); list_remove(elem); continue; } elem->new = elem->old + 1; } else if(strcasecmp(elem->old, "inbox") == 0) { elem->new = "INBOX"; } else { elem->new = elem->old; } /* i18n naming compatibility (RFC 3501 sect. 5.1.3) */ for(c = 0; elem->new[c] != '\0'; c++) { if(elem->new[c] < ' ' || elem->new[c] > 0x7E) { warnx("ignoring mailbox %s: " "I can't handle unencoded special characters", elem->old); list_remove(elem); continue; } /* encode ampersands */ if(elem->new[c] == '&') elem->new = xasprintf("%.*s&-%s", c, elem->new, elem->new+c+1); } /* strip mail and Mail from pathnames */ while(strncmp(elem->new, "mail/", 5) == 0 || strncmp(elem->new, "Mail/", 5) == 0) elem->new += 5; /* And add it back if necessary */ if(mailprefix) elem->new = xasprintf("%smail/%s", servername, elem->new); else elem->new = xasprintf("%s%s", servername, elem->new); debug(1, "server mailbox: %s", elem->new); } /* connect to server but don't open a mailbox */ debug(1, "doing mail_open %s", servername); serverstream = mail_open(NIL, servername, (debuglevel > 1 ? OP_DEBUG : NIL) | OP_HALFOPEN); if(serverstream == NIL) errx(1, "mail_open %s failed", servername); /* read list of remote mailboxes for avoiding clashes */ debug(1, "doing mail_list %s *", servername); mail_list(serverstream, servername, "*"); /* deal with name clashes */ list_qsort(clientlist, list_elem_cmp_new); elem = list_first(clientlist); while(!list_attail(elem)) { debug(2, "checking %s -> %s", elem->old, elem->new); /* find the end of this group of local mailboxes */ for(next = list_next(elem); !list_attail(next); next = list_next(next)) if(strcmp(elem->new, next->new) != 0) break; else debug(2, "matches %s -> %s", next->old, next->new); /* is there a local clash or a remote clash? */ srv = list_find(list_first(serverlist), elem->new, list_elem_cmp_str); if(srv != NULL) debug(2, "clash with %s", srv->old); if(list_next(elem) == next && srv == NULL) { debug(2, "no clash"); elem = next; continue; } /* even if we aren't renaming, we might be forced to */ if(!renameclashes) { if(srv == NULL || (srv->attr & LATT_NOSELECT) == 0) { elem = next; continue; } warnx("unselectable server mailbox: %s", srv->old); } /* number each clashing mailbox */ c = 0; while(elem != next) { do { s = xasprintf("%s-%d", elem->new, c++); srv = list_find(list_first(serverlist), s, list_elem_cmp_str); if(srv != NULL) debug(2, "clash with %s", s); } while(srv != NULL); elem->new = s; debug(1, "mailbox %s now %s", elem->old, elem->new); elem = list_next(elem); } } /* tell the user what we plan to do */ if(!quiet) { printf("Existing mailboxes on the server %s\n", servername); for(elem = list_first(serverlist); !list_attail(elem); elem = list_next(elem)) printf("\t%s\n", elem->old); printf("Local mailboxes to be uploaded, and their\n" "destination mailboxes on the server...\n"); for(elem = list_first(clientlist); !list_attail(elem); elem = list_next(elem)) printf("\t%s\n\t->\t%s\n", elem->old, elem->new); } fflush(stdout); sleep(1); /* confirm with the user that it's ok */ while(!confirmed) { FILE *yn; yn = fopen("/dev/tty", "r+"); if(yn == NULL) err(1, "opening terminal"); fprintf(yn, "Proceed? (y/n) "); xfgetline(yn, buffer, sizeof(buffer)); for(s = buffer; isspace(*(unsigned const char *)s); s++); if(*s == '\0') continue; if(*s != 'y' && *s != 'Y') errx(2, "not proceeding"); confirmed = true; } if(noaction) { debug(1, "doing mail_close %s", servername); serverstream = mail_close(serverstream); return(0); } for(elem = list_first(clientlist); !list_attail(elem); elem = list_next(elem)) { MAILSTREAM *newstream; FILE *fo, *ft; bool clean = true; /* GAH. dirty fix for NUL characters in mailboxes */ fo = fopen(elem->old, "r"); if(fo == NULL) { warn("open %s", elem->old); warnx("mailbox clean check failed; skipping %s -> %s", elem->old, elem->new); continue; } for(;;) { c = getc(fo); if(c == EOF) break; if(c == '\0') clean = false; } fclose(fo); if(clean) debug(1, "mailbox %s is clean", elem->old); else { debug(1, "mailbox %s is dirty", elem->old); elem->tmp = xasprintf("%s.%d.tmp", elem->old, getpid()); debug(1, "temporary filename: %s", elem->tmp); fo = fopen(elem->old, "r"); if(fo == NULL) warn("open %s", elem->old); ft = fopen(elem->tmp, "w"); if(ft == NULL) warn("open %s", elem->tmp); if(fo == NULL || ft == NULL) { if(fo != NULL) fclose(fo); if(ft != NULL) fclose(ft); warnx("mailbox cleaning failed; " "skipping %s -> %s", elem->old, elem->new); unlink(elem->tmp); continue; } if(fchmod(fileno(ft), 0600) < 0) warn("chmod 0600 %s", elem->tmp); for(;;) { c = getc(fo); if(c == EOF) break; if(c == '\0') putc('?', ft); else putc(c, ft); } if(fclose(fo) == EOF || fclose(ft) == EOF) { warn("copy"); warnx("mailbox cleaning failed; " "skipping %s -> %s", elem->old, elem->new); unlink(elem->tmp); continue; } } /* ensure the server mailbox exists before opening it */ if(list_find(list_first(serverlist), elem->new, list_elem_cmp_str) == NULL) { debug(1, "mail_create %s", elem->new); mail_create(serverstream, elem->new); srv = list_create_elem(elem->new); list_insert_last(serverlist, srv); } debug(1, "mail_open %s", elem->new); newstream = mail_open(serverstream, elem->new, (debuglevel > 1 ? OP_DEBUG : NIL)); if(newstream == NIL) { warnx("remote mail_open failed; skipping %s -> %s", elem->old, elem->new); if(elem->tmp != NULL) unlink(elem->tmp); continue; } else { serverstream = newstream; } /* now open the local mailbox */ debug(1, "mail_open %s", elem->tmp != NULL ? elem->tmp : elem->old); clientstream = mail_open(NIL, elem->tmp != NULL ? elem->tmp : elem->old, (debuglevel > 1 ? OP_DEBUG : NIL) | OP_READONLY); if(clientstream == NIL) { warnx("local mail_open failed; skipping %s -> %s", elem->old, elem->new); if(elem->tmp != NULL) unlink(elem->tmp); continue; } /* cache some metadata */ debug(1, "mailbox has %d messages", clientstream->nmsgs); mail_fetchfast(clientstream, xasprintf("1:%d", clientstream->nmsgs)); /* do the copy */ if(clientstream->nmsgs == 0) { warnx("%s contains no messages", elem->old); } else { warnx("uploading %s to %s", elem->old, elem->new); msgnum = 0; if(mail_append_multiple(serverstream, elem->new, appendmessage, NULL) == NIL) warnx("upload failed!"); } clientstream = mail_close(clientstream); if(elem->tmp != NULL) unlink(elem->tmp); } subs_file = fopen(".mailboxlist", "r"); if(subs_file != NULL) { debug(1, "uploading local subscription list"); while(fgetline(subs_file, buffer, sizeof(buffer))) { /* chomp any home directory prefix off the mailbox name and translate in the same way as we did before */ if(strncmp(buffer, "/home/", 6) == 0 && strncmp(buffer+6, username, strlen(username)) == 0 && buffer[strlen(username)+6] == '/') s = buffer + strlen(username) + 7; else if(strncmp(buffer, "~/", 2) == 0) s = buffer + 2; else s = buffer; debug(3, "mangled %s to %s", buffer, s); if(strcmp(buffer, "INBOX") != 0) { elem = list_find(list_first(clientlist), s, list_elem_cmp_str); if(elem != NULL) { debug(2, "%s -> %s", elem->old, elem->new); mail_subscribe(serverstream, elem->new); } else { debug(2, "ignoring %s", buffer); } } else { /* all the various local versions of INBOX */ s = xasprintf("/var/mail/%s", username); debug(3, "checking %s", s); elem = list_find(list_first(clientlist), s, list_elem_cmp_str); if(elem != NULL) { debug(2, "%s -> %s", s, elem->new); mail_subscribe(serverstream, elem->new); } s = xasprintf("/var/spool/mail/%s", username); debug(3, "checking %s", s); elem = list_find(list_first(clientlist), s, list_elem_cmp_str); if(elem != NULL) { debug(2, "%s -> %s", s, elem->new); mail_subscribe(serverstream, elem->new); } s = xasprintf("%sINBOX", servername); debug(2, "%s -> %s", buffer, s); mail_subscribe(serverstream, s); } } fclose(subs_file); debug(2, "finished with subscription list"); } debug(1, "doing mail_close %s", servername); serverstream = mail_close(serverstream); return(0); } /*! * \} */ /**********************************************************************/ /*! * \name message strings * \{ */ static void msg_string_init(STRING *s, void *data, unsigned long size) { /* remember the length of the header in data1 */ mail_fetchheader_full(clientstream, msgnum, NIL, &s->data1, FT_PREFETCHTEXT); mail_fetchtext_full(clientstream, msgnum, &s->size, NIL); s->size += s->data1; SETPOS(s, 0); } static char msg_string_next(STRING *s) { char c = *s->curpos++; SETPOS(s, GETPOS(s)); return(c); } static void msg_string_setpos(STRING *s, unsigned long i) { if (i < s->data1) { /* want header */ s->chunk = mail_fetchheader(clientstream, msgnum); s->chunksize = s->data1; s->offset = 0; } else if (i < s->size) { /* want body */ s->chunk = mail_fetchtext(clientstream, msgnum); s->chunksize = s->size - s->data1; s->offset = s->data1; } else { /* off end of message */ s->chunk = NIL; s->chunksize = 1; s->offset = i; } i -= s->offset; s->curpos = s->chunk + i; s->cursize = s->chunksize - i; } STRINGDRIVER msg_string = { msg_string_init, msg_string_next, msg_string_setpos }; /*! * \} */ /**********************************************************************/ /*! * \name c-client call-back functions. * \{ */ static long appendmessage(MAILSTREAM *stream, void *data, char **flags, char **date, STRING **message) { MESSAGECACHE *elt; unsigned long uflg, i; /* free data from the previous message */ if(msgflags != NULL) { free(msgflags); msgflags = NULL; } if(msgdate != NULL) { free(msgdate); msgdate = NULL; } mail_gc(clientstream, GC_TEXTS); /* get this message */ msgnum++; if(msgnum > clientstream->nmsgs) { *message = NIL; return(T); } debug(1, "append message %d", msgnum); elt = mail_elt(clientstream, msgnum); /* format the message's flags */ buffer[0] = '\0'; buffer[1] = '\0'; if(elt->seen) strcat(buffer, " \\Seen"); if(elt->deleted) strcat(buffer, " \\Deleted"); if(elt->flagged) strcat(buffer, " \\Flagged"); if(elt->answered) strcat(buffer, " \\Answered"); if(elt->draft) strcat(buffer, " \\Draft"); uflg = elt->user_flags; for(;;) { i = find_rightmost_bit(&uflg); if(i == -1) break; strcat(buffer, " "); strcat(buffer, clientstream->user_flags[i]); } *flags = msgflags = xstrdup(buffer + 1); debug(2, "message flags %s", msgflags); mail_date(buffer, elt); *date = msgdate = xstrdup(buffer); debug(2, "message date %s", msgdate); *message = &msgstr; INIT(&msgstr, msg_string, NULL, elt->rfc822_size); return(T); } void mm_searched(MAILSTREAM *stream, unsigned long msgno) { debug(2, "mm_searched()"); } void mm_exists(MAILSTREAM *stream, unsigned long number) { debug(2, "mm_exists()"); } void mm_expunged(MAILSTREAM *stream, unsigned long number) { debug(2, "mm_expunged()"); } void mm_flags(MAILSTREAM *stream, unsigned long number) { debug(2, "mm_flags()"); } void mm_list(MAILSTREAM *stream, int delim, char *name, long attr) { struct elem *elem; debug(2, "mm_list: delim %c name %s attr %s%s%s%s", delim, name, attr & LATT_NOINFERIORS ? "\\Noinferiors" : "", attr & LATT_NOSELECT ? "\\Noselect" : "", attr & LATT_MARKED ? "\\Marked" : "", attr & LATT_UNMARKED ? "\\Unmarked" : ""); elem = list_create_elem(name); elem->attr = attr; list_insert_last(serverlist, elem); debug(1, "Added to serverlist: %s", name); } void mm_lsub(MAILSTREAM *stream, int delimiter, char *name, long attributes) { debug(2, "mm_lsub()"); } void mm_status(MAILSTREAM *stream, char *mailbox, MAILSTATUS *status) { debug(2, "mm_status()"); } void mm_notify(MAILSTREAM *stream, char *string, long errflg) { mm_log(string, errflg); } void mm_log(char *string, long errflg) { switch(errflg) { case BYE: case NIL: if(debuglevel > 0) warnx("info: %s", string); break; case PARSE: case WARN: warnx("warning: %s", string); break; case ERROR: default: warnx("error: %s", string); break; } } void mm_dlog(char *string) { debug(2, "%s", string); } void mm_login(NETMBX *mb, char *u, char *p, long trial) { strcpy(u, username); strcpy(p, password); } void mm_critical(MAILSTREAM *stream) { critical = T; } void mm_nocritical(MAILSTREAM *stream) { critical = NIL; } long mm_diskerror(MAILSTREAM *stream, long errcode, long serious) { return(T); } void mm_fatal(char *string) { warnx("mm_fatal: %s", string); } /*! * \} */ /**********************************************************************/ /* EOF 2hermes/imapupload.c */