| #include "common.h" |
| #include <ctype.h> |
| #include <plumb.h> |
| #include <libsec.h> |
| #include <auth.h> |
| #include "dat.h" |
| |
| #pragma varargck argpos imap4cmd 2 |
| #pragma varargck type "Z" char* |
| |
| int doublequote(Fmt*); |
| int pipeline = 1; |
| |
| /* static char Eio[] = "i/o error"; jpc */ |
| |
| typedef struct Imap Imap; |
| struct Imap { |
| char *freep; /* free this to free the strings below */ |
| |
| char *host; |
| char *user; |
| char *mbox; |
| |
| int mustssl; |
| int refreshtime; |
| int debug; |
| |
| ulong tag; |
| ulong validity; |
| int nmsg; |
| int size; |
| char *base; |
| char *data; |
| |
| vlong *uid; |
| int nuid; |
| int muid; |
| |
| Thumbprint *thumb; |
| |
| /* open network connection */ |
| Biobuf bin; |
| Biobuf bout; |
| int fd; |
| }; |
| |
| static char* |
| removecr(char *s) |
| { |
| char *r, *w; |
| |
| for(r=w=s; *r; r++) |
| if(*r != '\r') |
| *w++ = *r; |
| *w = '\0'; |
| return s; |
| } |
| |
| /* */ |
| /* send imap4 command */ |
| /* */ |
| static void |
| imap4cmd(Imap *imap, char *fmt, ...) |
| { |
| char buf[128], *p; |
| va_list va; |
| |
| va_start(va, fmt); |
| p = buf+sprint(buf, "9X%lud ", imap->tag); |
| vseprint(p, buf+sizeof(buf), fmt, va); |
| va_end(va); |
| |
| p = buf+strlen(buf); |
| if(p > (buf+sizeof(buf)-3)) |
| sysfatal("imap4 command too long"); |
| |
| if(imap->debug) |
| fprint(2, "-> %s\n", buf); |
| strcpy(p, "\r\n"); |
| Bwrite(&imap->bout, buf, strlen(buf)); |
| Bflush(&imap->bout); |
| } |
| |
| enum { |
| OK, |
| NO, |
| BAD, |
| BYE, |
| EXISTS, |
| STATUS, |
| FETCH, |
| UNKNOWN |
| }; |
| |
| static char *verblist[] = { |
| [OK] "OK", |
| [NO] "NO", |
| [BAD] "BAD", |
| [BYE] "BYE", |
| [EXISTS] "EXISTS", |
| [STATUS] "STATUS", |
| [FETCH] "FETCH" |
| }; |
| |
| static int |
| verbcode(char *verb) |
| { |
| int i; |
| char *q; |
| |
| if(q = strchr(verb, ' ')) |
| *q = '\0'; |
| |
| for(i=0; i<nelem(verblist); i++) |
| if(verblist[i] && strcmp(verblist[i], verb)==0){ |
| if(q) |
| *q = ' '; |
| return i; |
| } |
| if(q) |
| *q = ' '; |
| return UNKNOWN; |
| } |
| |
| static void |
| strupr(char *s) |
| { |
| for(; *s; s++) |
| if('a' <= *s && *s <= 'z') |
| *s += 'A'-'a'; |
| } |
| |
| static void |
| imapgrow(Imap *imap, int n) |
| { |
| int i; |
| |
| if(imap->data == nil){ |
| imap->base = emalloc(n+1); |
| imap->data = imap->base; |
| imap->size = n+1; |
| } |
| if(n >= imap->size){ |
| /* friggin microsoft - reallocate */ |
| i = imap->data - imap->base; |
| imap->base = erealloc(imap->base, i+n+1); |
| imap->data = imap->base + i; |
| imap->size = n+1; |
| } |
| } |
| |
| |
| /* */ |
| /* get imap4 response line. there might be various */ |
| /* data or other informational lines mixed in. */ |
| /* */ |
| static char* |
| imap4resp(Imap *imap) |
| { |
| char *line, *p, *ep, *op, *q, *r, *en, *verb; |
| int i, n; |
| static char error[256]; |
| |
| while(p = Brdline(&imap->bin, '\n')){ |
| ep = p+Blinelen(&imap->bin); |
| while(ep > p && (ep[-1]=='\n' || ep[-1]=='\r')) |
| *--ep = '\0'; |
| |
| if(imap->debug) |
| fprint(2, "<- %s\n", p); |
| strupr(p); |
| |
| switch(p[0]){ |
| case '+': |
| if(imap->tag == 0) |
| fprint(2, "unexpected: %s\n", p); |
| break; |
| |
| /* ``unsolicited'' information; everything happens here. */ |
| case '*': |
| if(p[1]!=' ') |
| continue; |
| p += 2; |
| line = p; |
| n = strtol(p, &p, 10); |
| if(*p==' ') |
| p++; |
| verb = p; |
| |
| if(p = strchr(verb, ' ')) |
| p++; |
| else |
| p = verb+strlen(verb); |
| |
| switch(verbcode(verb)){ |
| case OK: |
| case NO: |
| case BAD: |
| /* human readable text at p; */ |
| break; |
| case BYE: |
| /* early disconnect */ |
| /* human readable text at p; */ |
| break; |
| |
| /* * 32 EXISTS */ |
| case EXISTS: |
| imap->nmsg = n; |
| break; |
| |
| /* * STATUS Inbox (MESSAGES 2 UIDVALIDITY 960164964) */ |
| case STATUS: |
| if(q = strstr(p, "MESSAGES")) |
| imap->nmsg = atoi(q+8); |
| if(q = strstr(p, "UIDVALIDITY")) |
| imap->validity = strtoul(q+11, 0, 10); |
| break; |
| |
| case FETCH: |
| /* * 1 FETCH (uid 8889 RFC822.SIZE 3031 body[] {3031} */ |
| /* <3031 bytes of data> */ |
| /* ) */ |
| if(strstr(p, "RFC822.SIZE") && strstr(p, "BODY[]")){ |
| if((q = strchr(p, '{')) |
| && (n=strtol(q+1, &en, 0), *en=='}')){ |
| if(imap->data == nil || n >= imap->size) |
| imapgrow(imap, n); |
| if((i = Bread(&imap->bin, imap->data, n)) != n){ |
| snprint(error, sizeof error, |
| "short read %d != %d: %r\n", |
| i, n); |
| return error; |
| } |
| if(imap->debug) |
| fprint(2, "<- read %d bytes\n", n); |
| imap->data[n] = '\0'; |
| if(imap->debug) |
| fprint(2, "<- %s\n", imap->data); |
| imap->data += n; |
| imap->size -= n; |
| p = Brdline(&imap->bin, '\n'); |
| if(imap->debug) |
| fprint(2, "<- ignoring %.*s\n", |
| Blinelen(&imap->bin), p); |
| }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){ |
| *r = '\0'; |
| q++; |
| n = r-q; |
| if(imap->data == nil || n >= imap->size) |
| imapgrow(imap, n); |
| memmove(imap->data, q, n); |
| imap->data[n] = '\0'; |
| imap->data += n; |
| imap->size -= n; |
| }else |
| return "confused about FETCH response"; |
| break; |
| } |
| |
| /* * 1 FETCH (UID 1 RFC822.SIZE 511) */ |
| if(q=strstr(p, "RFC822.SIZE")){ |
| imap->size = atoi(q+11); |
| break; |
| } |
| |
| /* * 1 FETCH (UID 1 RFC822.HEADER {496} */ |
| /* <496 bytes of data> */ |
| /* ) */ |
| /* * 1 FETCH (UID 1 RFC822.HEADER "data") */ |
| if(strstr(p, "RFC822.HEADER") || strstr(p, "RFC822.TEXT")){ |
| if((q = strchr(p, '{')) |
| && (n=strtol(q+1, &en, 0), *en=='}')){ |
| if(imap->data == nil || n >= imap->size) |
| imapgrow(imap, n); |
| if((i = Bread(&imap->bin, imap->data, n)) != n){ |
| snprint(error, sizeof error, |
| "short read %d != %d: %r\n", |
| i, n); |
| return error; |
| } |
| if(imap->debug) |
| fprint(2, "<- read %d bytes\n", n); |
| imap->data[n] = '\0'; |
| if(imap->debug) |
| fprint(2, "<- %s\n", imap->data); |
| imap->data += n; |
| imap->size -= n; |
| p = Brdline(&imap->bin, '\n'); |
| if(imap->debug) |
| fprint(2, "<- ignoring %.*s\n", |
| Blinelen(&imap->bin), p); |
| }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){ |
| *r = '\0'; |
| q++; |
| n = r-q; |
| if(imap->data == nil || n >= imap->size) |
| imapgrow(imap, n); |
| memmove(imap->data, q, n); |
| imap->data[n] = '\0'; |
| imap->data += n; |
| imap->size -= n; |
| }else |
| return "confused about FETCH response"; |
| break; |
| } |
| |
| /* * 1 FETCH (UID 1) */ |
| /* * 2 FETCH (UID 6) */ |
| if(q = strstr(p, "UID")){ |
| if(imap->nuid < imap->muid) |
| imap->uid[imap->nuid++] = ((vlong)imap->validity<<32)|strtoul(q+3, nil, 10); |
| break; |
| } |
| } |
| |
| if(imap->tag == 0) |
| return line; |
| break; |
| |
| case '9': /* response to our message */ |
| op = p; |
| if(p[1]=='X' && strtoul(p+2, &p, 10)==imap->tag){ |
| while(*p==' ') |
| p++; |
| imap->tag++; |
| return p; |
| } |
| fprint(2, "expected %lud; got %s\n", imap->tag, op); |
| break; |
| |
| default: |
| if(imap->debug || *p) |
| fprint(2, "unexpected line: %s\n", p); |
| } |
| } |
| snprint(error, sizeof error, "i/o error: %r\n"); |
| return error; |
| } |
| |
| static int |
| isokay(char *resp) |
| { |
| return strncmp(resp, "OK", 2)==0; |
| } |
| |
| /* */ |
| /* log in to IMAP4 server, select mailbox, no SSL at the moment */ |
| /* */ |
| static char* |
| imap4login(Imap *imap) |
| { |
| char *s; |
| UserPasswd *up; |
| |
| imap->tag = 0; |
| s = imap4resp(imap); |
| if(!isokay(s)) |
| return "error in initial IMAP handshake"; |
| |
| if(imap->user != nil) |
| up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q user=%q", imap->host, imap->user); |
| else |
| up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q", imap->host); |
| if(up == nil) |
| return "cannot find IMAP password"; |
| |
| imap->tag = 1; |
| imap4cmd(imap, "LOGIN %Z %Z", up->user, up->passwd); |
| free(up); |
| if(!isokay(s = imap4resp(imap))) |
| return s; |
| |
| imap4cmd(imap, "SELECT %Z", imap->mbox); |
| if(!isokay(s = imap4resp(imap))) |
| return s; |
| |
| return nil; |
| } |
| |
| /* */ |
| /* push tls onto a connection */ |
| /* */ |
| int |
| mypushtls(int fd) |
| { |
| int p[2]; |
| char buf[10]; |
| |
| if(pipe(p) < 0) |
| return -1; |
| |
| switch(fork()){ |
| case -1: |
| close(p[0]); |
| close(p[1]); |
| return -1; |
| case 0: |
| close(p[1]); |
| dup(p[0], 0); |
| dup(p[0], 1); |
| sprint(buf, "/fd/%d", fd); |
| execl("/bin/tlsrelay", "tlsrelay", "-f", buf, nil); |
| _exits(nil); |
| default: |
| break; |
| } |
| close(fd); |
| close(p[0]); |
| return p[1]; |
| } |
| |
| /* */ |
| /* dial and handshake with the imap server */ |
| /* */ |
| static char* |
| imap4dial(Imap *imap) |
| { |
| char *err, *port; |
| uchar digest[SHA1dlen]; |
| int sfd; |
| TLSconn conn; |
| |
| if(imap->fd >= 0){ |
| imap4cmd(imap, "noop"); |
| if(isokay(imap4resp(imap))) |
| return nil; |
| close(imap->fd); |
| imap->fd = -1; |
| } |
| |
| if(imap->mustssl) |
| port = "imaps"; |
| else |
| port = "imap"; |
| |
| if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0) |
| return geterrstr(); |
| |
| if(imap->mustssl){ |
| memset(&conn, 0, sizeof conn); |
| sfd = tlsClient(imap->fd, &conn); |
| if(sfd < 0) |
| sysfatal("tlsClient: %r"); |
| if(conn.cert==nil || conn.certlen <= 0) |
| sysfatal("server did not provide TLS certificate"); |
| sha1(conn.cert, conn.certlen, digest, nil); |
| if(!imap->thumb || !okThumbprint(digest, imap->thumb)){ |
| fmtinstall('H', encodefmt); |
| sysfatal("server certificate %.*H not recognized", SHA1dlen, digest); |
| } |
| free(conn.cert); |
| close(imap->fd); |
| imap->fd = sfd; |
| |
| if(imap->debug){ |
| char fn[128]; |
| int fd; |
| |
| snprint(fn, sizeof fn, "%s/ctl", conn.dir); |
| fd = open(fn, ORDWR); |
| if(fd < 0) |
| fprint(2, "opening ctl: %r\n"); |
| if(fprint(fd, "debug") < 0) |
| fprint(2, "writing ctl: %r\n"); |
| close(fd); |
| } |
| } |
| Binit(&imap->bin, imap->fd, OREAD); |
| Binit(&imap->bout, imap->fd, OWRITE); |
| |
| if(err = imap4login(imap)) { |
| close(imap->fd); |
| return err; |
| } |
| |
| return nil; |
| } |
| |
| /* */ |
| /* close connection */ |
| /* */ |
| #if 0 /* jpc */ |
| static void |
| imap4hangup(Imap *imap) |
| { |
| imap4cmd(imap, "LOGOUT"); |
| imap4resp(imap); |
| close(imap->fd); |
| } |
| #endif |
| |
| /* */ |
| /* download a single message */ |
| /* */ |
| static char* |
| imap4fetch(Mailbox *mb, Message *m) |
| { |
| int i; |
| char *p, *s, sdigest[2*SHA1dlen+1]; |
| Imap *imap; |
| |
| imap = mb->aux; |
| |
| imap->size = 0; |
| |
| if(!isokay(s = imap4resp(imap))) |
| return s; |
| |
| p = imap->base; |
| if(p == nil) |
| return "did not get message body"; |
| |
| removecr(p); |
| free(m->start); |
| m->start = p; |
| m->end = p+strlen(p); |
| m->bend = m->rbend = m->end; |
| m->header = m->start; |
| |
| imap->base = nil; |
| imap->data = nil; |
| |
| parse(m, 0, mb, 1); |
| |
| /* digest headers */ |
| sha1((uchar*)m->start, m->end - m->start, m->digest, nil); |
| for(i = 0; i < SHA1dlen; i++) |
| sprint(sdigest+2*i, "%2.2ux", m->digest[i]); |
| m->sdigest = s_copy(sdigest); |
| |
| return nil; |
| } |
| |
| /* */ |
| /* check for new messages on imap4 server */ |
| /* download new messages, mark deleted messages */ |
| /* */ |
| static char* |
| imap4read(Imap *imap, Mailbox *mb, int doplumb) |
| { |
| char *s; |
| int i, ignore, nnew, t; |
| Message *m, *next, **l; |
| |
| imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox); |
| if(!isokay(s = imap4resp(imap))) |
| return s; |
| |
| imap->nuid = 0; |
| imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0])); |
| imap->muid = imap->nmsg; |
| |
| if(imap->nmsg > 0){ |
| imap4cmd(imap, "UID FETCH 1:* UID"); |
| if(!isokay(s = imap4resp(imap))) |
| return s; |
| } |
| |
| l = &mb->root->part; |
| for(i=0; i<imap->nuid; i++){ |
| ignore = 0; |
| while(*l != nil){ |
| if((*l)->imapuid == imap->uid[i]){ |
| ignore = 1; |
| l = &(*l)->next; |
| break; |
| }else{ |
| /* old mail, we don't have it anymore */ |
| if(doplumb) |
| mailplumb(mb, *l, 1); |
| (*l)->inmbox = 0; |
| (*l)->deleted = 1; |
| l = &(*l)->next; |
| } |
| } |
| if(ignore) |
| continue; |
| |
| /* new message */ |
| m = newmessage(mb->root); |
| m->mallocd = 1; |
| m->inmbox = 1; |
| m->imapuid = imap->uid[i]; |
| |
| /* add to chain, will download soon */ |
| *l = m; |
| l = &m->next; |
| } |
| |
| /* whatever is left at the end of the chain is gone */ |
| while(*l != nil){ |
| if(doplumb) |
| mailplumb(mb, *l, 1); |
| (*l)->inmbox = 0; |
| (*l)->deleted = 1; |
| l = &(*l)->next; |
| } |
| |
| /* download new messages */ |
| t = imap->tag; |
| if(pipeline) |
| switch(rfork(RFPROC|RFMEM)){ |
| case -1: |
| sysfatal("rfork: %r"); |
| default: |
| break; |
| case 0: |
| for(m = mb->root->part; m != nil; m = m->next){ |
| if(m->start != nil) |
| continue; |
| if(imap->debug) |
| fprint(2, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n", |
| t, (ulong)m->imapuid); |
| Bprint(&imap->bout, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n", |
| t++, (ulong)m->imapuid); |
| } |
| Bflush(&imap->bout); |
| _exits(nil); |
| } |
| |
| nnew = 0; |
| for(m=mb->root->part; m!=nil; m=next){ |
| next = m->next; |
| if(m->start != nil) |
| continue; |
| |
| if(!pipeline){ |
| Bprint(&imap->bout, "9X%lud UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n", |
| (ulong)imap->tag, (ulong)m->imapuid); |
| Bflush(&imap->bout); |
| } |
| |
| if(s = imap4fetch(mb, m)){ |
| /* message disappeared? unchain */ |
| fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s); |
| delmessage(mb, m); |
| mb->root->subname--; |
| continue; |
| } |
| nnew++; |
| if(doplumb) |
| mailplumb(mb, m, 0); |
| } |
| if(pipeline) |
| waitpid(); |
| |
| if(nnew || mb->vers == 0){ |
| mb->vers++; |
| henter(PATH(0, Qtop), mb->name, |
| (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb); |
| } |
| return nil; |
| } |
| |
| /* */ |
| /* sync mailbox */ |
| /* */ |
| static void |
| imap4purge(Imap *imap, Mailbox *mb) |
| { |
| int ndel; |
| Message *m, *next; |
| |
| ndel = 0; |
| for(m=mb->root->part; m!=nil; m=next){ |
| next = m->next; |
| if(m->deleted && m->refs==0){ |
| if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){ |
| imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid); |
| if(isokay(imap4resp(imap))){ |
| ndel++; |
| delmessage(mb, m); |
| } |
| }else |
| delmessage(mb, m); |
| } |
| } |
| |
| if(ndel){ |
| imap4cmd(imap, "EXPUNGE"); |
| imap4resp(imap); |
| } |
| } |
| |
| /* */ |
| /* connect to imap4 server, sync mailbox */ |
| /* */ |
| static char* |
| imap4sync(Mailbox *mb, int doplumb) |
| { |
| char *err; |
| Imap *imap; |
| |
| imap = mb->aux; |
| |
| if(err = imap4dial(imap)){ |
| mb->waketime = time(0) + imap->refreshtime; |
| return err; |
| } |
| |
| if((err = imap4read(imap, mb, doplumb)) == nil){ |
| imap4purge(imap, mb); |
| mb->d->atime = mb->d->mtime = time(0); |
| } |
| /* |
| * don't hang up; leave connection open for next time. |
| */ |
| /* imap4hangup(imap); */ |
| mb->waketime = time(0) + imap->refreshtime; |
| return err; |
| } |
| |
| static char Eimap4ctl[] = "bad imap4 control message"; |
| |
| static char* |
| imap4ctl(Mailbox *mb, int argc, char **argv) |
| { |
| int n; |
| Imap *imap; |
| |
| imap = mb->aux; |
| if(argc < 1) |
| return Eimap4ctl; |
| |
| if(argc==1 && strcmp(argv[0], "debug")==0){ |
| imap->debug = 1; |
| return nil; |
| } |
| |
| if(argc==1 && strcmp(argv[0], "nodebug")==0){ |
| imap->debug = 0; |
| return nil; |
| } |
| |
| if(argc==1 && strcmp(argv[0], "thumbprint")==0){ |
| if(imap->thumb) |
| freeThumbprints(imap->thumb); |
| imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); |
| } |
| if(strcmp(argv[0], "refresh")==0){ |
| if(argc==1){ |
| imap->refreshtime = 60; |
| return nil; |
| } |
| if(argc==2){ |
| n = atoi(argv[1]); |
| if(n < 15) |
| return Eimap4ctl; |
| imap->refreshtime = n; |
| return nil; |
| } |
| } |
| |
| return Eimap4ctl; |
| } |
| |
| /* */ |
| /* free extra memory associated with mb */ |
| /* */ |
| static void |
| imap4close(Mailbox *mb) |
| { |
| Imap *imap; |
| |
| imap = mb->aux; |
| free(imap->freep); |
| free(imap->base); |
| free(imap->uid); |
| if(imap->fd >= 0) |
| close(imap->fd); |
| free(imap); |
| } |
| |
| /* */ |
| /* open mailboxes of the form /imap/host/user */ |
| /* */ |
| char* |
| imap4mbox(Mailbox *mb, char *path) |
| { |
| char *f[10]; |
| int mustssl, nf; |
| Imap *imap; |
| |
| quotefmtinstall(); |
| fmtinstall('Z', doublequote); |
| if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0) |
| return Enotme; |
| mustssl = (strncmp(path, "/imaps/", 7) == 0); |
| |
| path = strdup(path); |
| if(path == nil) |
| return "out of memory"; |
| |
| nf = getfields(path, f, 5, 0, "/"); |
| if(nf < 3){ |
| free(path); |
| return "bad imap path syntax /imap[s]/system[/user[/mailbox]]"; |
| } |
| |
| imap = emalloc(sizeof(*imap)); |
| imap->fd = -1; |
| imap->debug = debug; |
| imap->freep = path; |
| imap->mustssl = mustssl; |
| imap->host = f[2]; |
| if(nf < 4) |
| imap->user = nil; |
| else |
| imap->user = f[3]; |
| if(nf < 5) |
| imap->mbox = "Inbox"; |
| else |
| imap->mbox = f[4]; |
| imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); |
| |
| mb->aux = imap; |
| mb->sync = imap4sync; |
| mb->close = imap4close; |
| mb->ctl = imap4ctl; |
| mb->d = emalloc(sizeof(*mb->d)); |
| /*mb->fetch = imap4fetch; */ |
| |
| return nil; |
| } |
| |
| /* */ |
| /* Formatter for %" */ |
| /* Use double quotes to protect white space, frogs, \ and " */ |
| /* */ |
| enum |
| { |
| Qok = 0, |
| Qquote, |
| Qbackslash |
| }; |
| |
| static int |
| needtoquote(Rune r) |
| { |
| if(r >= Runeself) |
| return Qquote; |
| if(r <= ' ') |
| return Qquote; |
| if(r=='\\' || r=='"') |
| return Qbackslash; |
| return Qok; |
| } |
| |
| int |
| doublequote(Fmt *f) |
| { |
| char *s, *t; |
| int w, quotes; |
| Rune r; |
| |
| s = va_arg(f->args, char*); |
| if(s == nil || *s == '\0') |
| return fmtstrcpy(f, "\"\""); |
| |
| quotes = 0; |
| for(t=s; *t; t+=w){ |
| w = chartorune(&r, t); |
| quotes |= needtoquote(r); |
| } |
| if(quotes == 0) |
| return fmtstrcpy(f, s); |
| |
| fmtrune(f, '"'); |
| for(t=s; *t; t+=w){ |
| w = chartorune(&r, t); |
| if(needtoquote(r) == Qbackslash) |
| fmtrune(f, '\\'); |
| fmtrune(f, r); |
| } |
| return fmtrune(f, '"'); |
| } |