summaryrefslogtreecommitdiff
path: root/sys/src/cmd/upas/spf
diff options
context:
space:
mode:
authorcinap_lenrek <cinap_lenrek@felloff.net>2017-03-12 17:15:03 +0100
committercinap_lenrek <cinap_lenrek@felloff.net>2017-03-12 17:15:03 +0100
commit963cfc9a6f6e721f52aa949e6d1af0c3e8dc2ecc (patch)
tree749b74875dbc49bcf6ed0776648b8f0ef9417407 /sys/src/cmd/upas/spf
parent8177d20fb2709ba9290dfd41308b8e5bee4e00f8 (diff)
merging erik quanstros nupas
Diffstat (limited to 'sys/src/cmd/upas/spf')
-rw-r--r--sys/src/cmd/upas/spf/dns.c81
-rw-r--r--sys/src/cmd/upas/spf/macro.c304
-rw-r--r--sys/src/cmd/upas/spf/mkfile14
-rw-r--r--sys/src/cmd/upas/spf/mtest.c39
-rw-r--r--sys/src/cmd/upas/spf/spf.c800
-rw-r--r--sys/src/cmd/upas/spf/spf.h12
-rw-r--r--sys/src/cmd/upas/spf/testsuite9
7 files changed, 1259 insertions, 0 deletions
diff --git a/sys/src/cmd/upas/spf/dns.c b/sys/src/cmd/upas/spf/dns.c
new file mode 100644
index 000000000..01b2b2c43
--- /dev/null
+++ b/sys/src/cmd/upas/spf/dns.c
@@ -0,0 +1,81 @@
+#include "spf.h"
+
+extern char dflag;
+extern char vflag;
+extern char *netroot;
+
+static int
+timeout(void*, char *msg)
+{
+ if(strstr(msg, "alarm")){
+ fprint(2, "deferred: dns timeout");
+ exits("deferred: dns timeout");
+ }
+ return 0;
+}
+
+static Ndbtuple*
+tdnsquery(char *r, char *s, char *v)
+{
+ long a;
+ Ndbtuple *t;
+
+ atnotify(timeout, 1);
+ a = alarm(15*1000);
+ t = dnsquery(r, s, v);
+ alarm(a);
+ atnotify(timeout, 0);
+ return t;
+}
+
+Ndbtuple*
+vdnsquery(char *s, char *v, int recur)
+{
+ Ndbtuple *n, *t;
+ static int nquery;
+
+ /* conflicts with standard: must limit to 10 and -> fail */
+ if(recur > 5 || ++nquery == 25){
+ fprint(2, "dns query limited %d %d\n", recur, nquery);
+ return 0;
+ }
+ if(dflag)
+ fprint(2, "dnsquery(%s, %s, %s) ->\n", netroot, s, v);
+ t = tdnsquery(netroot, s, v);
+ if(dflag)
+ for(n = t; n; n = n->entry)
+ fprint(2, "\t%s\t%s\n", n->attr, n->val);
+ return t;
+}
+
+void
+dnreverse(char *s, int l, char *d)
+{
+ char *p, *e, buf[100], *f[15];
+ int i, n;
+
+ n = getfields(d, f, nelem(f), 0, ".");
+ p = e = buf;
+ if(l < sizeof buf)
+ e += l;
+ else
+ e += sizeof buf;
+ for(i = 1; i <= n; i++)
+ p = seprint(p, e, "%s.", f[n-i]);
+ if(p > buf)
+ p = seprint(p-1, e, ".in-addr.arpa");
+ memmove(s, buf, p-buf+1);
+}
+
+int
+dncontains(char *d, char *s)
+{
+loop:
+ if(!strcmp(d, s))
+ return 1;
+ if(!(s = strchr(s, '.')))
+ return 0;
+ s++;
+ goto loop;
+}
+
diff --git a/sys/src/cmd/upas/spf/macro.c b/sys/src/cmd/upas/spf/macro.c
new file mode 100644
index 000000000..2ccc8e3b8
--- /dev/null
+++ b/sys/src/cmd/upas/spf/macro.c
@@ -0,0 +1,304 @@
+#include "spf.h"
+
+#define mrprint(...) snprint(m->mreg, sizeof m->mreg, __VA_ARGS__)
+
+typedef struct Mfmt Mfmt;
+typedef struct Macro Macro;
+
+struct Mfmt{
+ char buf[0xff];
+ char *p;
+ char *e;
+
+ char mreg[0xff];
+ int f1;
+ int f2;
+ int f3;
+
+ char *sender;
+ char *domain;
+ char *ip;
+ char *helo;
+ uchar ipa[IPaddrlen];
+};
+
+struct Macro{
+ char c;
+ void (*f)(Mfmt*);
+};
+
+static void
+ms(Mfmt *m)
+{
+ mrprint("%s", m->sender);
+}
+
+static void
+ml(Mfmt *m)
+{
+ char *p;
+
+ mrprint("%s", m->sender);
+ if(p = strchr(m->mreg, '@'))
+ *p = 0;
+}
+
+static void
+mo(Mfmt *m)
+{
+ mrprint("%s", m->domain);
+}
+
+static void
+md(Mfmt *m)
+{
+ mrprint("%s", m->domain);
+}
+
+static void
+mi(Mfmt *m)
+{
+ uint i, c;
+
+ if(isv4(m->ipa))
+ mrprint("%s", m->ip);
+ else{
+ for(i = 0; i < 32; i++){
+ c = m->ipa[i / 2];
+ if((i & 1) == 0)
+ c >>= 4;
+ sprint(m->mreg+2*i, "%ux.", c & 0xf);
+ }
+ m->mreg[2*32 - 1] = 0;
+ }
+}
+
+static int
+maquery(Mfmt *m, char *d, char *match, int recur)
+{
+ int r;
+ Ndbtuple *t, *n;
+
+ r = 0;
+ t = vdnsquery(d, "any", recur);
+ for(n = t; n; n = n->entry)
+ if(!strcmp(n->attr, "ip") || !strcmp(n->attr, "ipv6")){
+ if(!strcmp(n->val, match)){
+ r = 1;
+ break;
+ }
+ }else if(!strcmp(n->attr, "cname"))
+ maquery(m, d, match, recur+1);
+ ndbfree(t);
+ return r;
+}
+
+static int
+lrcmp(char *a, char *b)
+{
+ return strlen(b) - strlen(a);
+}
+
+static void
+mptrquery(Mfmt *m, char *d, int recur)
+{
+ char *s, buf[64], *a, *list[11];
+ int nlist, i;
+ Ndbtuple *t, *n;
+
+ nlist = 0;
+ dnreverse(buf, sizeof buf, s = strdup(m->ip));
+ t = vdnsquery(buf, "ptr", recur);
+ for(n = t; n; n = n->entry){
+ if(!strcmp(n->attr, "dom") || !strcmp(n->attr, "cname"))
+ if(dncontains(n->val, d) && maquery(m, n->val, m->ip, recur+1))
+ list[nlist++] = strdup(n->val);
+ }
+ ndbfree(t);
+ free(s);
+ qsort(list, nlist, sizeof *list, (int(*)(void*,void*))lrcmp);
+ a = "unknown";
+ for(i = 0; i < nlist; i++)
+ if(!strcmp(list[i], d)){
+ a = list[i];
+ break;
+ }else if(dncontains(list[i], d))
+ a = list[i];
+ mrprint("%s", a);
+ for(i = 0; i < nlist; i++)
+ free(list[i]);
+}
+
+static void
+mp(Mfmt *m)
+{
+ /*
+ * we're supposed to do a reverse lookup on the ip & compare.
+ * this is a very bad idea.
+ */
+// mrprint("unknown); /* simulate dns failure */
+ mptrquery(m, m->domain, 0);
+}
+
+static void
+mv(Mfmt *m)
+{
+ if(isv4(m->ipa))
+ mrprint("in-addr");
+ else
+ mrprint("ip6");
+}
+
+static void
+mh(Mfmt *m)
+{
+ mrprint("%s", m->helo);
+}
+
+static Macro tab[] = {
+'s', ms, /* sender */
+'l', ml, /* local part of sender */
+'o', mo, /* domain of sender */
+'d', md, /* domain */
+'i', mi, /* ip */
+'p', mp, /* validated domain name of ip */
+'v', mv, /* "in-addr" if ipv4, or "ip6" if ipv6 */
+'h', mh, /* helo/ehol domain */
+};
+
+static void
+reverse(Mfmt *m)
+{
+ char *p, *e, buf[100], *f[32], sep[2];
+ int i, n;
+
+ sep[0] = m->f2;
+ sep[1] = 0;
+ n = getfields(m->mreg, f, nelem(f), 0, sep);
+ p = e = buf;
+ e += sizeof buf-1;
+ for(i = 0; i < n; i++)
+ p = seprint(p, e, "%s.", f[n-i-1]);
+ if(p > buf)
+ p--;
+ *p = 0;
+ memmove(m->mreg, buf, p-buf+1);
+ m->f2 = '.';
+}
+
+static void
+chop(Mfmt *m)
+{
+ char *p, *e, buf[100], *f[32], sep[2];
+ int i, n;
+
+ sep[0] = m->f2;
+ sep[1] = 0;
+ n = getfields(m->mreg, f, nelem(f), 0, sep);
+ p = e = buf;
+ e += sizeof buf-1;
+ if(m->f1 == 0)
+ i = 0;
+ else
+ i = n-m->f1;
+ if(i < 0)
+ i = 0;
+ for(; i < n; i++)
+ p = seprint(p, e, "%s.", f[i]);
+ if(p > buf)
+ p--;
+ *p = 0;
+ memmove(m->mreg, buf, p-buf+1);
+ m->f2 = '.';
+}
+
+static void
+mfmtinit(Mfmt *m, char *s, char *d, char *h, char *i)
+{
+ memset(m, 0, sizeof *m);
+ m->p = m->buf;
+ m->e = m->p + sizeof m->buf-1;
+ m->sender = s? s: "Unsets";
+ m->domain = d? d: "Unsetd";
+ m->helo = h? h: "Unseth";
+ m->ip = i? i: "127.0.0.2";
+ parseip(m->ipa, m->ip);
+}
+
+/* url escaping? rfc3986 */
+static void
+mputc(Mfmt *m, int c)
+{
+ if(m->p < m->e)
+ *m->p++ = c;
+}
+
+static void
+mputs(Mfmt *m, char *s)
+{
+ int c;
+
+ while(c = *s++)
+ mputc(m, c);
+}
+
+char*
+macro(char *f, char *sender, char *dom, char *hdom, char *ip)
+{
+ char *p;
+ int i, c;
+ Mfmt m;
+
+ mfmtinit(&m, sender, dom, hdom, ip);
+ while(*f){
+ while((c = *f++) && c != '%')
+ mputc(&m, c);
+ if(c == 0)
+ break;
+ switch(*f++){
+ case '%':
+ mputc(&m, '%');
+ break;
+ case '-':
+ mputs(&m, "%20");
+ break;
+ case '_':
+ mputc(&m, ' ');
+ break;
+ case '{':
+ m.f1 = 0;
+ m.f2 = '.';
+ m.f3 = 0;
+ c = *f++;
+ if(c >= 'A' && c <= 'Z')
+ c += 0x20;
+ for(i = 0; i < nelem(tab); i++)
+ if(tab[i].c == c)
+ break;
+ if(i == nelem(tab))
+ return 0;
+ for(c = *f++; c >= '0' && c <= '9'; c = *f++)
+ m.f1 = m.f1*10 + c-'0';
+ if(c == 'R' || c == 'r'){
+ m.f3 = 'r';
+ c = *f++;
+ }
+ for(; p = strchr(".-+,_=", c); c = *f++)
+ m.f2 = *p;
+ if(c == '}'){
+ tab[i].f(&m);
+ if(m.f1 || m.f2 != '.')
+ chop(&m);
+ if(m.f3 == 'r')
+ reverse(&m);
+ mputs(&m, m.mreg);
+ m.mreg[0] = 0;
+ break;
+ }
+ default:
+ return 0;
+ }
+ }
+ mputc(&m, 0);
+ return strdup(m.buf);
+}
diff --git a/sys/src/cmd/upas/spf/mkfile b/sys/src/cmd/upas/spf/mkfile
new file mode 100644
index 000000000..f612714f9
--- /dev/null
+++ b/sys/src/cmd/upas/spf/mkfile
@@ -0,0 +1,14 @@
+</$objtype/mkfile
+<../mkupas
+
+TARG=spf
+
+OFILES=\
+ dns.$O\
+ macro.$O\
+ spf.$O\
+
+</sys/src/cmd/mkone
+
+mtest: dns.$O macro.$O mtest.$O
+ $LD $LDFLAGS -o $target $prereq
diff --git a/sys/src/cmd/upas/spf/mtest.c b/sys/src/cmd/upas/spf/mtest.c
new file mode 100644
index 000000000..3bd0e14f2
--- /dev/null
+++ b/sys/src/cmd/upas/spf/mtest.c
@@ -0,0 +1,39 @@
+#include "spf.h"
+
+char dflag;
+char vflag;
+char *netroot = "/net";
+
+void
+usage(void)
+{
+ fprint(2, "usage: mtest [-dv] sender dom hello ip\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ char *a[5], *s;
+ int i;
+
+ ARGBEGIN{
+ case 'd':
+ dflag = 1;
+ break;
+ case 'v':
+ vflag = 1;
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ fmtinstall('I', eipfmt);
+ memset(a, 0, sizeof a);
+ for(i = 0; i < argc && i < nelem(a); i++)
+ a[i] = argv[i];
+ s = macro(a[0], a[1], a[2], a[3], a[4]);
+ print("%s\n", s);
+ free(s);
+ exits("");
+}
diff --git a/sys/src/cmd/upas/spf/spf.c b/sys/src/cmd/upas/spf/spf.c
new file mode 100644
index 000000000..b76db76a2
--- /dev/null
+++ b/sys/src/cmd/upas/spf/spf.c
@@ -0,0 +1,800 @@
+#include "spf.h"
+
+#define vprint(...) if(vflag) fprint(2, __VA_ARGS__)
+
+enum{
+ Traw,
+ Tip4,
+ Tip6,
+ Texists,
+ Tall,
+ Tbegin,
+ Tend,
+};
+
+char *typetab[] = {
+ "raw",
+ "ip4",
+ "ip6",
+ "exists",
+ "all",
+ "begin",
+ "end",
+};
+
+typedef struct Squery Squery;
+struct Squery{
+ char ver;
+ char sabort;
+ char mod;
+ char *cidrtail;
+ char *ptrmatch;
+ char *ip;
+ char *domain;
+ char *sender;
+ char *hello;
+};
+
+typedef struct Spf Spf;
+struct Spf{
+ char mod;
+ char type;
+ char s[100];
+};
+#pragma varargck type "§" Spf*
+
+char *txt;
+char *netroot = "/net";
+char dflag;
+char eflag;
+char mflag;
+char pflag;
+char rflag;
+char vflag;
+
+char *vtab[] = {0, "v=spf1", "spf2.0/"};
+
+char*
+isvn(Squery *q, char *s, int i)
+{
+ char *p, *t;
+
+ t = vtab[i];
+ if(cistrncmp(s, t, strlen(t)))
+ return 0;
+ p = s + strlen(t);
+ if(i == 2){
+ p = strchr(p, ' ');
+ if(p == nil)
+ return 0;
+ }
+ if(*p && *p++ != ' ')
+ return 0;
+ q->ver = i;
+ return p;
+}
+
+char*
+pickspf(Squery *s, char *v1, char *v2)
+{
+ switch(s->ver){
+ default:
+ case 0:
+ if(v1)
+ return v1;
+ return v2;
+ case 1:
+ if(v1)
+ return v1;
+ return 0;
+ case 2:
+ if(v2)
+ return v2;
+ return v1; /* spf2.0/pra,mfrom */
+ }
+}
+
+char *ftab[] = {"txt", "spf"}; /* p. 9 */
+
+char*
+spffetch(Squery *s, char *d)
+{
+ char *p, *v1, *v2;
+ int i;
+ Ndbtuple *t, *n;
+
+ if(txt){
+ p = strdup(txt);
+ txt = 0;
+ return p;
+ }
+ v1 = v2 = 0;
+ for(i = 0; i < nelem(ftab); i++){
+ t = vdnsquery(d, ftab[i], 0);
+ for(n = t; n; n = n->entry){
+ if(strcmp(n->attr, ftab[i]))
+ continue;
+ v1 = isvn(s, n->val, 1);
+ v2 = isvn(s, n->val, 2);
+ }
+ if(p = pickspf(s, v1, v2))
+ p = strdup(p);
+ ndbfree(t);
+ if(p)
+ return p;
+ }
+ return 0;
+}
+
+Spf spftab[200];
+int nspf;
+int mod;
+
+Spf*
+spfadd(int type, char *s)
+{
+ Spf *p;
+
+ if(nspf >= nelem(spftab))
+ return 0;
+ p = spftab+nspf;
+ p->s[0] = 0;
+ if(s)
+ snprint(p->s, sizeof p->s, "%s", s);
+ p->type = type;
+ p->mod = mod;
+ nspf++;
+ return p;
+}
+
+char *badcidr[] = {
+ "0.0.0.0/8",
+ "1.0.0.0/8",
+ "2.0.0.0/8",
+ "5.0.0.0/8",
+ "10.0.0.0/8",
+ "127.0.0.0/8",
+ "255.0.0.0/8",
+ "192.168.0.0/16",
+ "169.254.0.0/16",
+ "172.16.0.0/20",
+ "224.0.0.0/24", /*rfc 3330 says this is /4. not sure */
+ "fc00::/7",
+};
+
+char *okcidr[] = {
+ "17.0.0.0/8", /* apple. seems dubious. */
+};
+
+int
+parsecidr(uchar *addr, uchar *mask, char *from)
+{
+ char *p, buf[50];
+ int i, bits, z;
+ vlong v;
+ uchar *a;
+
+ strecpy(buf, buf+sizeof buf, from);
+ if(p = strchr(buf, '/'))
+ *p = 0;
+ v = parseip(addr, buf);
+ if(v == -1)
+ return -1;
+ switch((ulong)v){
+ default:
+ bits = 32;
+ z = 96;
+ break;
+ case 6:
+ bits = 128;
+ z = 0;
+ break;
+ }
+
+ if(p){
+ i = strtoul(p+1, &p, 0);
+ if(i > bits)
+ i = bits;
+ i += z;
+ memset(mask, 0, 128/8);
+ for(a = mask; i >= 8; i -= 8)
+ *a++ = 0xff;
+ if(i > 0)
+ *a = ~((1<<(8-i))-1);
+ }else
+ memset(mask, 0xff, IPaddrlen);
+ return 0;
+}
+
+/*
+ * match x.y.z.w to x1.y1.z1.w1/m
+ */
+int
+cidrmatch(char *x, char *y)
+{
+ uchar a[IPaddrlen], b[IPaddrlen], m[IPaddrlen];
+
+ if(parseip(a, x) == -1)
+ return 0;
+ parsecidr(b, m, y);
+ maskip(a, m, a);
+ maskip(b, m, b);
+ if(!memcmp(a, b, IPaddrlen))
+ return 1;
+ return 0;
+}
+
+int
+cidrmatchtab(char *addr, char **tab, int ntab)
+{
+ int i;
+
+ for(i = 0; i < ntab; i++)
+ if(cidrmatch(addr, tab[i]))
+ return 1;
+ return 0;
+}
+
+int
+okcidrlen(char *cidr, int i)
+{
+ if(i >= 14 && i <= 128)
+ return 1;
+ if(cidrmatchtab(cidr, okcidr, nelem(okcidr)))
+ return 1;
+ return 0;
+}
+
+int
+cidrokay0(char *cidr)
+{
+ char *p, buf[40];
+ uchar addr[IPaddrlen];
+ int l, i;
+
+ p = strchr(cidr, '/');
+ if(p)
+ l = p-cidr;
+ else
+ l = strlen(cidr);
+ if(l > 39)
+ return 0;
+ if(p){
+ i = atoi(p+1);
+ if(!okcidrlen(cidr, i))
+ return 0;
+ }
+ memcpy(buf, cidr, l);
+ buf[l] = 0;
+ if(parseip(addr, buf) == -1)
+ return 0;
+ if(cidrmatchtab(cidr, badcidr, nelem(badcidr)))
+ return 0;
+ return 1;
+}
+
+int
+cidrokay(char *cidr)
+{
+ if(!cidrokay0(cidr)){
+ fprint(2, "spf: naughty cidr %s\n", cidr);
+ return 0;
+ }
+ return 1;
+}
+
+int
+ptrmatch(Squery *q, char *s)
+{
+ if(!q->ptrmatch || !strcmp(q->ptrmatch, s))
+ return 1;
+ return 0;
+}
+
+Spf*
+spfaddcidr(Squery *q, int type, char *s)
+{
+ char buf[64];
+
+ if(q->cidrtail){
+ snprint(buf, sizeof buf, "%s/%s", s, q->cidrtail);
+ s = buf;
+ }
+ if(cidrokay(s) && ptrmatch(q, s))
+ return spfadd(type, s);
+ return 0;
+}
+
+char*
+qpluscidr(Squery *q, char *d, int recur, int *y)
+{
+ char *p;
+
+ *y = 0;
+ if(!recur && (p = strchr(d, '/'))){
+ q->cidrtail = p + 1;
+ *p = 0;
+ *y = 1;
+ }
+ return d;
+}
+
+void
+cidrtail(Squery *q, char *, int y)
+{
+ if(!y)
+ return;
+ q->cidrtail[-1] = '/';
+ q->cidrtail = 0;
+}
+
+void
+aquery(Squery *q, char *d, int recur)
+{
+ int y;
+ Ndbtuple *t, *n;
+
+ d = qpluscidr(q, d, recur, &y);
+ t = vdnsquery(d, "any", recur);
+ for(n = t; n; n = n->entry){
+ if(!strcmp(n->attr, "ip"))
+ spfaddcidr(q, Tip4, n->val);
+ else if(!strcmp(n->attr, "ipv6"))
+ spfaddcidr(q, Tip6, n->val);
+ else if(!strcmp(n->attr, "cname"))
+ aquery(q, d, recur+1);
+ }
+ cidrtail(q, d, y);
+ ndbfree(t);
+}
+
+void
+mxquery(Squery *q, char *d, int recur)
+{
+ int i, y;
+ Ndbtuple *t, *n;
+
+ d = qpluscidr(q, d, recur, &y);
+ i = 0;
+ t = vdnsquery(d, "mx", recur);
+ for(n = t; n; n = n->entry)
+ if(i++ < 10 && !strcmp(n->attr, "mx"))
+ aquery(q, n->val, recur+1);
+ ndbfree(t);
+ cidrtail(q, d, y);
+}
+
+void
+ptrquery(Squery *q, char *d, int recur)
+{
+ char *s, buf[64];
+ int i, y;
+ Ndbtuple *t, *n;
+
+ if(!q->ip){
+ fprint(2, "spf: ptr query; no ip\n");
+ return;
+ }
+ d = qpluscidr(q, d, recur, &y);
+ i = 0;
+ dnreverse(buf, sizeof buf, s = strdup(q->ip));
+ t = vdnsquery(buf, "ptr", recur);
+ for(n = t; n; n = n->entry){
+ if(!strcmp(n->attr, "dom") || !strcmp(n->attr, "cname"))
+ if(i++ < 10 && dncontains(d, n->val)){
+ q->ptrmatch = q->ip;
+ aquery(q, n->val, recur+1);
+ q->ptrmatch = 0;
+ }
+ }
+ ndbfree(t);
+ free(s);
+ cidrtail(q, d, y);
+}
+
+/*
+ * this looks very wrong; see §5.7 which says only a records match.
+ */
+void
+exists(Squery*, char *d, int recur)
+{
+ Ndbtuple *t;
+
+ if(t = vdnsquery(d, "ip", recur))
+ spfadd(Texists, "1");
+ else
+ spfadd(Texists, 0);
+ ndbfree(t);
+}
+
+void
+addfail(void)
+{
+ mod = '-';
+ spfadd(Tall, 0);
+}
+
+void
+addend(char *s)
+{
+ spfadd(Tend, s);
+ spftab[nspf-1].mod = 0;
+}
+
+Spf*
+includeloop(char *s1, int n)
+{
+ char *s, *p;
+ int i;
+
+ for(i = 0; i < n; i++){
+ s = spftab[i].s;
+ if(s)
+ if(p = strstr(s, " -> "))
+ if(!strcmp(p+4, s1))
+ return spftab+i;
+ }
+ return nil;
+}
+
+void
+addbegin(int c, char *s0, char *s1)
+{
+ char buf[0xff];
+
+ snprint(buf, sizeof buf, "%s -> %s", s0, s1);
+ spfadd(Tbegin, buf);
+ spftab[nspf-1].mod = c;
+}
+
+void
+ditch(void)
+{
+ if(nspf > 0)
+ nspf--;
+}
+
+static void
+lower(char *s)
+{
+ int c;
+
+ for(; c = *s; s++)
+ if(c >= 'A' && c <= 'Z')
+ *s = c + 0x20;
+}
+
+int
+spfquery(Squery *x, char *d, int include)
+{
+ char *s, **t, *r, *p, *q, buf[10];
+ int i, n, c;
+ Spf *inc;
+
+ if(include)
+ if(inc = includeloop(d, nspf-1)){
+ fprint(2, "spf: include loop: %s (%s)\n", d, inc->s);
+ return -1;
+ }
+ s = spffetch(x, d);
+ if(!s)
+ return -1;
+ t = malloc(500*sizeof *t);
+ n = getfields(s, t, 500, 1, " ");
+ x->sabort = 0;
+ for(i = 0; i < n && !x->sabort; i++){
+ if(!strncmp(t[i], "v=", 2))
+ continue;
+ c = *t[i];
+ r = t[i]+1;
+ switch(c){
+ default:
+ mod = '+';
+ r--;
+ break;
+ case '-':
+ case '~':
+ case '+':
+ case '?':
+ mod = c;
+ break;
+ }
+ if(!strcmp(r, "all")){
+ spfadd(Tall, 0);
+ continue;
+ }
+ strecpy(buf, buf+sizeof buf, r);
+ p = strchr(buf, ':');
+ if(p == 0)
+ p = strchr(buf, '=');
+ q = d;
+ if(p){
+ *p = 0;
+ q = p+1;
+ q = r+(q-buf);
+ }
+ if(!mflag)
+ q = macro(q, x->sender, x->domain, x->hello, x->ip);
+ else
+ q = strdup(q);
+ lower(buf);
+ if(!strcmp(buf, "ip4"))
+ spfaddcidr(x, Tip4, q);
+ else if(!strcmp(buf, "ip6"))
+ spfaddcidr(x, Tip6, q);
+ else if(!strcmp(buf, "a"))
+ aquery(x, q, 0);
+ else if(!strcmp(buf, "mx"))
+ mxquery(x, d, 0);
+ else if(!strcmp(buf, "ptr"))
+ ptrquery(x, d, 0);
+ else if(!strcmp(buf, "exists"))
+ exists(x, q, 0);
+ else if(!strcmp(buf, "include") || !strcmp(buf, "redirect")){
+ if(q && *q){
+ if(rflag)
+ fprint(2, "I> %s\n", q);
+ addbegin(mod, r, q);
+ if(spfquery(x, q, 1) == -1){
+ ditch();
+ addfail();
+ }else
+ addend(r);
+ }
+ }
+ free(q);
+ }
+ free(t);
+ free(s);
+ return 0;
+}
+
+char*
+url(char *s)
+{
+ char buf[64], *p, *e;
+ int c;
+
+ p = buf;
+ e = p + sizeof buf;
+ *p = 0;
+ while(c = *s++){
+ if(c >= 'A' && c <= 'Z')
+ c += 0x20;
+ if(c <= ' ' || c == '%' || c & 0x80)
+ p = seprint(p, e, "%%%2.2X", c);
+ else
+ p = seprint(p, e, "%c", c);
+ }
+ return strdup(buf);
+}
+
+void
+spfinit(Squery *q, char *dom, int argc, char **argv)
+{
+ uchar a[IPaddrlen];
+
+ memset(q, 0, sizeof q);
+ q->ip = argc>0? argv[1]: 0;
+ if(q->ip && parseip(a, q->ip) == -1)
+ sysfatal("bogus ip");
+ q->domain = url(dom);
+ q->sender = argc>2? url(argv[2]): 0;
+ q->hello = argc>3? url(argv[3]): 0;
+ mod = 0; /* BOTCH */
+}
+
+int
+§fmt(Fmt *f)
+{
+ char *p, *e, buf[115];
+ Spf *spf;
+
+ spf = va_arg(f->args, Spf*);
+ if(!spf)
+ return fmtstrcpy(f, "<nil>");
+ e = buf+sizeof buf;
+ p = buf;
+ if(spf->mod && spf->mod != '+')
+ *p++ = spf->mod;
+ p = seprint(p, e, "%s", typetab[spf->type]);
+ if(spf->s[0])
+ seprint(p, e, " : %s", spf->s);
+ return fmtstrcpy(f, buf);
+}
+
+static Spf head;
+
+struct{
+ int i;
+}walk;
+
+int
+invertmod(int c)
+{
+ switch(c){
+ case '?':
+ return '?';
+ case '+':
+ return '-';
+ case '-':
+ return '+';
+ case '~':
+ return '?';
+ }
+ return 0;
+}
+
+#define reprint(...) if(vflag && recur == 0) fprint(2, __VA_ARGS__)
+
+int
+spfwalk(int all, int recur, char *ip)
+{
+ int match, bias, mod, r;
+ Spf *s;
+
+ r = 0;
+ bias = 0;
+ if(recur == 0)
+ walk.i = 0;
+ for(; walk.i < nspf; walk.i++){
+ s = spftab+walk.i;
+ mod = s->mod;
+ switch(s->type){
+ default:
+ abort();
+ case Tbegin:
+ walk.i++;
+ match = spfwalk(s->s[0] == 'r', recur+1, ip);
+ if(match < 0)
+ mod = invertmod(mod);
+ break;
+ case Tend:
+ return r;
+ case Tall:
+ match = 1;
+ break;
+ case Texists:
+ match = s->s[0];
+ break;
+ case Tip4:
+ case Tip6:
+ match = cidrmatch(ip, s->s);
+ break;
+ }
+ if(!r && match)
+ switch(mod){
+ case '~':
+ reprint("bias %§\n", s);
+ bias = '~';
+ case '?':
+ break;
+ case '-':
+ if(all || s->type !=Tall){
+ vprint("fail %§\n", s);
+ r = -1;
+ }
+ break;
+ case '+':
+ default:
+ vprint("match %§\n", s);
+ r = 1;
+ }
+ }
+ /* recur == 0 */
+ if(r == 0 && bias == '~')
+ r = -1;
+ return r;
+}
+
+/* ad hoc and noncomprehensive */
+char *tccld[] = {"au", "ca", "gt", "id", "pk", "uk", "ve", };
+int
+is3cctld(char *s)
+{
+ int i;
+
+ if(strlen(s) != 2)
+ return 0;
+ for(i = 0; i < nelem(tccld); i++)
+ if(!strcmp(tccld[i], s))
+ return 1;
+ return 0;
+}
+
+char*
+rootify(char *d)
+{
+ char *p, *q;
+
+ if(!(p = strchr(d, '.')))
+ return 0;
+ p++;
+ if(!(q = strchr(p, '.')))
+ return 0;
+ q++;
+ if(!strchr(q, '.') && is3cctld(q))
+ return 0;
+ return p;
+}
+
+void
+usage(void)
+{
+ fprint(2, "spf [-demrpv] [-n netroot] dom [ip sender helo]\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ char *s, *d, *e;
+ int i, j, t[] = {0, 3};
+ Squery q;
+
+ ARGBEGIN{
+ case 'd':
+ dflag = 1;
+ break;
+ case 'e':
+ eflag = 1;
+ break;
+ case 'm':
+ mflag = 1;
+ break;
+ case 'n':
+ netroot = EARGF(usage());
+ break;
+ case 'p':
+ pflag = 1;
+ break;
+ case 'r':
+ rflag = 1;
+ break;
+ case 't':
+ txt = EARGF(usage());
+ break;
+ case 'v':
+ vflag = 1;
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ if(argc < 1 || argc > 4)
+ usage();
+ if(argc == 1)
+ pflag = 1;
+ fmtinstall(L'§', §fmt);
+ fmtinstall('I', eipfmt);
+ fmtinstall('M', eipfmt);
+
+ e = "none";
+ for(i = 0; i < nelem(t); i++){
+ if(argc <= t[i])
+ break;
+ d = argv[t[i]];
+ for(j = 0; j < i; j++)
+ if(!strcmp(argv[t[j]], d))
+ goto loop;
+ for(s = d; ; s = rootify(s)){
+ if(!s)
+ goto loop;
+ spfinit(&q, d, argc, argv); /* or s? */
+ addbegin('+', ".", s);
+ if(spfquery(&q, s, 0) != -1)
+ break;
+ }
+ if(eflag && nspf)
+ addfail();
+ e = "";
+ if(pflag)
+ for(j = 0; j < nspf; j++)
+ print("%§\n", spftab+j);
+ if(argc >= t[i] && argc > 1)
+ if(spfwalk(1, 0, argv[1]) == -1)
+ exits("fail");
+loop:;
+ }
+ exits(e);
+}
diff --git a/sys/src/cmd/upas/spf/spf.h b/sys/src/cmd/upas/spf/spf.h
new file mode 100644
index 000000000..b37bad864
--- /dev/null
+++ b/sys/src/cmd/upas/spf/spf.h
@@ -0,0 +1,12 @@
+/* © 2008 erik quanstrom; plan 9 license */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+
+char *macro(char*, char*, char*, char*, char*);
+
+Ndbtuple* vdnsquery(char*, char*, int);
+int dncontains(char*, char *);
+void dnreverse(char*, int, char*);
diff --git a/sys/src/cmd/upas/spf/testsuite b/sys/src/cmd/upas/spf/testsuite
new file mode 100644
index 000000000..1b55f6c2a
--- /dev/null
+++ b/sys/src/cmd/upas/spf/testsuite
@@ -0,0 +1,9 @@
+#!/bin/rc
+for(i in '%{s}' '%{o}' '%{d}' '%{d4}' '%{d3}' '%{d2}' '%{d1}' '%{dr}' '%{d2r}' '%{l}' '%{l-}' '%{lr}' '%{lr-}' '%{l1r-}')
+ mtest $i 'strong-bad@email.example.com' email.example.com helounknown 192.0.2.3
+for(i in '%{i}')
+ mtest $i 'strong-bad@email.example.com' email.example.com helounknown 2001:db8::cb01
+for(i in '%{ir}.%{v}._spf.%{d2}' '%{lr-}.lp._spf.%{d2}' '%{lr-}.lp.%{ir}.%{v}._spf.%{d2}' '%{ir}.%{v}.%{lr-}.lp._spf.%{d2}')
+ mtest $i 'strong-bad@email.example.com' email.example.com helounknown 2001:db8::cb01
+for(i in '%{ir}.%{v}._spf.%{d2}' '%{lr-}.lp._spf.%{d2}' '%{lr-}.lp.%{ir}.%{v}._spf.%{d2}' '%{ir}.%{v}.%{lr-}.lp._spf.%{d2}')
+ mtest $i 'strong-bad@email.example.com' email.example.com helounknown 192.0.2.3