From 256870664833badf02ba69cb474a5efecb5c5fbb Mon Sep 17 00:00:00 2001 From: Neil Smith Date: Mon, 9 Jan 2017 09:52:06 +0000 Subject: [PATCH] Initial commit --- .gitignore | 5 + etc/default/opendmarc | 10 + etc/init.d/opendmarc | 151 ++++ etc/opendmarc.conf | 72 ++ opendmarc.sublime-project | 8 + usr/sbin/opendmarc | Bin 0 -> 74528 bytes usr/sbin/opendmarc-check | Bin 0 -> 10568 bytes usr/sbin/opendmarc-expire | 465 ++++++++++++ usr/sbin/opendmarc-import | 606 ++++++++++++++++ usr/sbin/opendmarc-importstats | 26 + usr/sbin/opendmarc-params | 284 ++++++++ usr/sbin/opendmarc-reports | 991 ++++++++++++++++++++++++++ usr/share/doc/opendmarc/README.Debian | 48 ++ usr/share/doc/opendmarc/README.schema | 39 + usr/share/doc/opendmarc/copyright | 170 +++++ usr/share/doc/opendmarc/mkdb.mysql | 77 ++ usr/share/doc/opendmarc/schema.mysql | 93 +++ 17 files changed, 3045 insertions(+) create mode 100644 .gitignore create mode 100644 etc/default/opendmarc create mode 100755 etc/init.d/opendmarc create mode 100644 etc/opendmarc.conf create mode 100644 opendmarc.sublime-project create mode 100755 usr/sbin/opendmarc create mode 100755 usr/sbin/opendmarc-check create mode 100755 usr/sbin/opendmarc-expire create mode 100755 usr/sbin/opendmarc-import create mode 100755 usr/sbin/opendmarc-importstats create mode 100755 usr/sbin/opendmarc-params create mode 100755 usr/sbin/opendmarc-reports create mode 100644 usr/share/doc/opendmarc/README.Debian create mode 100644 usr/share/doc/opendmarc/README.schema create mode 100644 usr/share/doc/opendmarc/copyright create mode 100644 usr/share/doc/opendmarc/mkdb.mysql create mode 100644 usr/share/doc/opendmarc/schema.mysql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df7cdd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Sublime text +*.sublime-workspace + +# Logs +*.log diff --git a/etc/default/opendmarc b/etc/default/opendmarc new file mode 100644 index 0000000..3a45664 --- /dev/null +++ b/etc/default/opendmarc @@ -0,0 +1,10 @@ +# Command-line options specified here will override the contents of +# /etc/opendmarc.conf. See opendmarc(8) for a complete list of options. +#DAEMON_OPTS="" +# +# Uncomment to specify an alternate socket +# Note that setting this will override any Socket value in opendkim.conf +#SOCKET="local:/var/run/opendmarc/opendmarc.sock" # default +#SOCKET="inet:54321" # listen on all interfaces on port 54321 +#SOCKET="inet:12345@localhost" # listen on loopback on port 12345 +#SOCKET="inet:12345@192.0.2.1" # listen on 192.0.2.1 on port 12345 diff --git a/etc/init.d/opendmarc b/etc/init.d/opendmarc new file mode 100755 index 0000000..186a394 --- /dev/null +++ b/etc/init.d/opendmarc @@ -0,0 +1,151 @@ +#! /bin/sh +# +### BEGIN INIT INFO +# Provides: opendmarc +# Required-Start: $syslog $time $local_fs $remote_fs $named $network +# Required-Stop: $syslog $time $local_fs $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start the OpenDMARC service +# Description: Enable DMAR verification and reporting provided by OpenDMARC +### END INIT INFO + +PATH=/sbin:/bin:/usr/sbin:/usr/bin +DAEMON=/usr/sbin/opendmarc +NAME=opendmarc +DESC="OpenDMARC" +RUNDIR=/var/run/$NAME +USER=opendmarc +GROUP=opendmarc +SOCKET=local:$RUNDIR/$NAME.sock +PIDFILE=$RUNDIR/$NAME.pid + +# How long to wait for the process to die on stop/restart +stoptimeout=5 + +test -x $DAEMON || exit 0 + +# Include LSB provided init functions +. /lib/lsb/init-functions + +# Include opendkim defaults if available +if [ -f /etc/default/opendmarc ] ; then + . /etc/default/opendmarc +fi + +if [ -f /etc/opendmarc.conf ]; then + CONFIG_SOCKET=`awk '$1 == "Socket" { print $2 }' /etc/opendmarc.conf` +fi + +# This can be set via Socket option in config file, so it's not required +if [ -n "$SOCKET" -a -z "$CONFIG_SOCKET" ]; then + DAEMON_OPTS="-p $SOCKET $DAEMON_OPTS" +fi + +DAEMON_OPTS="-c /etc/opendmarc.conf -u $USER -P $PIDFILE $DAEMON_OPTS" + +start() { + # Create the run directory if it doesn't exist + if [ ! -d "$RUNDIR" ]; then + install -o "$USER" -g "$GROUP" -m 755 -d "$RUNDIR" || return 2 + [ -x /sbin/restorecon ] && /sbin/restorecon "$RUNDIR" + fi + # Clean up stale sockets + if [ -f "$PIDFILE" ]; then + pid=`cat $PIDFILE` + if ! ps -C "$DAEMON" -s "$pid" >/dev/null; then + rm "$PIDFILE" + TMPSOCKET="" + if [ -n "$SOCKET" ]; then + TMPSOCKET="$SOCKET" + elif [ -n "$CONFIG_SOCKET" ]; then + TMPSOCKET="$CONFIG_SOCKET" + fi + if [ -n "$TMPSOCKET" ]; then + # UNIX sockets may be specified with or without the + # local: prefix; handle both + t=`echo $SOCKET | cut -d: -f1` + s=`echo $SOCKET | cut -d: -f2` + if [ -e "$s" -a -S "$s" ]; then + if [ "$t" = "$s" -o "$t" = "local" ]; then + rm "$s" + fi + fi + fi + fi + fi + start-stop-daemon --start --quiet --pidfile "$PIDFILE" --exec "$DAEMON" --test -- $DAEMON_OPTS || return 1 + start-stop-daemon --start --quiet --pidfile "$PIDFILE" --exec "$DAEMON" -- $DAEMON_OPTS || return 2 +} + +stop() { + start-stop-daemon --stop --retry "$stoptimeout" --exec "$DAEMON" + [ "$?" = 2 ] && return 2 +} + +reload() { + start-stop-daemon --stop --signal USR1 --exec "$DAEMON" +} + +status() { + local pidfile daemon name status + + pidfile= + OPTIND=1 + while getopts p: opt ; do + case "$opt" in + p) pidfile="$OPTARG";; + esac + done + shift $(($OPTIND - 1)) + + if [ -n "$pidfile" ]; then + pidfile="-p $pidfile" + fi + daemon="$1" + name="$2" + + status="0" + pidofproc $pidfile $daemon >/dev/null || status="$?" + if [ "$status" = 0 ]; then + log_success_msg "$name is running" + return 0 + else + log_failure_msg "$name is not running" + return $status + fi +} + +case "$1" in + start) + echo -n "Starting $DESC: " + start + echo "$NAME." + ;; + stop) + echo -n "Stopping $DESC: " + stop + echo "$NAME." + ;; + restart) + echo -n "Restarting $DESC: " + stop + start + echo "$NAME." + ;; + reload|force-reload) + echo -n "Restarting $DESC: " + reload + echo "$NAME." + ;; + status) + status $DAEMON $NAME + ;; + *) + N=/etc/init.d/$NAME + echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/etc/opendmarc.conf b/etc/opendmarc.conf new file mode 100644 index 0000000..e3067bf --- /dev/null +++ b/etc/opendmarc.conf @@ -0,0 +1,72 @@ +# This is a basic configuration that can easily be adapted to suit a standard +# installation. For more advanced options, see opendkim.conf(5) and/or +# /usr/share/doc/opendmarc/examples/opendmarc.conf.sample. + +## AuthservID (string) +## defaults to MTA name +# +# AuthservID name + +## FailureReports { true | false } +## default "false" +## +# FailureReports false + +PidFile /var/run/opendmarc.pid + +## RejectFailures { true | false } +## default "false" +## +RejectFailures false + +## Syslog { true | false } +## default "false" +## +## Log via calls to syslog(3) any interesting activity. +# +Syslog true + +## SyslogFacility facility-name +## default "mail" +## +## Log via calls to syslog(3) using the named facility. The facility names +## are the same as the ones allowed in syslog.conf(5). +# +# SyslogFacility mail + +## TrustedAuthservIDs string +## default HOSTNAME +## +## Specifies one or more "authserv-id" values to trust as relaying true +## upstream DKIM and SPF results. The default is to use the name of +## the MTA processing the message. To specify a list, separate each entry +## with a comma. The key word "HOSTNAME" will be replaced by the name of +## the host running the filter as reported by the gethostname(3) function. +# +# TrustedAuthservIDs HOSTNAME + + +## UMask mask +## default (none) +## +## Requests a specific permissions mask to be used for file creation. This +## only really applies to creation of the socket when Socket specifies a +## UNIX domain socket, and to the HistoryFile and PidFile (if any); temporary +## files are normally created by the mkstemp(3) function that enforces a +## specific file mode on creation regardless of the process umask. See +## umask(2) for more information. +# +UMask 0002 + +## UserID user[:group] +## default (none) +## +## Attempts to become the specified userid before starting operations. +## The process will be assigned all of the groups and primary group ID of +## the named userid unless an alternate group is specified. +# +UserID opendmarc:opendmarc + +## Path to system copy of PSL (needed to determine organizational domain) +# +PublicSuffixList /usr/share/publicsuffix/ diff --git a/opendmarc.sublime-project b/opendmarc.sublime-project new file mode 100644 index 0000000..24db303 --- /dev/null +++ b/opendmarc.sublime-project @@ -0,0 +1,8 @@ +{ + "folders": + [ + { + "path": "." + } + ] +} diff --git a/usr/sbin/opendmarc b/usr/sbin/opendmarc new file mode 100755 index 0000000000000000000000000000000000000000..96cd1be1da793475c77eac073fc2a630c4c4c444 GIT binary patch literal 74528 zcma&P31Ade@;}~z1OkE`xkN=Bb=07MiJ}GposmR(+@Mi##WP??AaZ5H44@zk&WzFy zqpYjD=(;ZI>iSjJMZYd4qlR!L;K|{EqJVgH6A=^z7S#MdpL#u$4l>{0KT2k*UcGwt zs_NCNcXVfdN$C|Gva+<)PY3N%jZoPrpPz zBjvoy&vNQnPQBD;o+E$iGu1!#jsM0tQT=Bzdv~n;k8bCneN;tOY$%@x z|0r`W{G$)BykX^g^R5~4cu7DH&AB7lb5FNlcK2YJiGSq5dJET{mKDyq`559B;ooBX z%RhNhzspNL`1Z9g9-Lcs-T1sMe;C`ZF7f@bLw<<&JAciA9=kU0Ire{_JkSHA8Grl` z>}Ui(K**!vr$Lyb;UgLFr)S_ZzVp%f49!6Q9Tal3c1LDtcWeg!<1_Hzl7Y{08R$1; zp#Kb-KAN1pF=&s5|2YHyp5Ws-iq(F}!2j_Ke6G)+w`(%!=gJIxx@Pc`2^sjW%z!V* zpojZ1^s9RYJ;yLGj;7BKGRPmz(C&f^a?Z=p-^L8>zL0@`b_V=|8T!?ffqrKO`b#p% ze{u$Vb_TqiL2o4)`nx=XJZm!Wc{T%|k_`Rbn}OcW(C*V2_#B&oJ~xB^Ps!kiOET!~ z?F{|BECc^(8RWbx1ODd>`13OG4`!h6l|c{hW$^PqX5jNk2Kwp@^yfjJ9{fA<$Dcu- z+cNNZD+7Lg2D^01z<)pn`foGvk7vM7%;29FXQ20G!2dggoEtOXYclxTB^l_S%7CxU zpobeW$oUiuc(izXO9p=!nSuZ58RWS(1OAZ=_`VtR-!FswPiN5EunhDkWEfvBXQ2N) zLx1x#@Tton&uJOx&&km4s~PxQ3VV5u9CUzJWZ-ju2KiT@&e80Ceg?a2&S00(8QP6y zkpFKP=-imkJ_DckGqmf?;OBYAyHiNx> z&46E+fzQqi{T-8mK0gEgl??j%F@v7-GRQeD1ASQr`kx^GaoUMm zouEDI;8S+EL+~eRMP5ap!h0P)8;;;J)uBI<&wTOoW3>FUeJK|5IT!L%9_^&|^1K1P z5x>RZ4^VzC6F$23{(7H-Q6C|P&yFMH(Xj5EUQ;=3!qoA#6>7Oz9vCyKd_rYy<;3b4 z!OGe(qee`bKCN=h_}ivbYUSkn75r20`S zr`sv7nLeeu;*Ro)>C>iFRs_pyXH*8Y-;`EW1}i3&*JLUfKclAVw`J%~?fBX|%B!bU zyID@G45k_lPA{*VRyDo0qVl)3X9TC$R+b-?v63bfte#SRXXS+Q3Dc*Jub!rrSD~Tb z@<~ZCV^SzMVfw6TzokfZ{RlZ7#V9$dYNt<4HPoh^;FJmFwUy&1plKMS4G*cygOxMf zy>__$Ayf&Ch=pDB6Q<25uNptO63i;5*G_PiIMq~-XJj_Ogvr%Ye_Kt-e)KZMa{QF) ziPNT5PCJUIj?F7F7S)Ev|F%h0t2T5Le@N*$Z3&oWYV{QOHJ!b1P{E)=ZN}89>Tv+Zj;S-pPf_6b2@`57YMg4~Si;~Bq4Bljrvr2+++0cD)vqAj7u_p&AX^t(nF8 z99OeS}Xi0UaC#HyY)Swp|3Kt9k-z)-J| zCdnF1CRa~^>4LRY!Ro1uiVS^-h*G;s`h<{LGmAbBs^Ih~T2_@KUJ$p z@vohN-qh4q1i`d=V&&AD;2o)uIA!`oZEEFIf)$e{R7)@Km;tOP1N@)_4V^w;tC&(b zz81!VI8`ov(Jm7vu)e5(?V>}|LqTWeQ|X-ceIoej6Q%5>!r`NxO}&#aIhsR7r>2 z{6Rz0LEzdc)l*?>F_XibF>l&*twsq7(n-^21kt5Rt!h?nHN6l62C-Q~YvZSgaVT2K zIfBzCm$N}76U;JwR%NY7hUR9VRd94#byT2@uc)Y;F@xgJdB_ZX#0MmwPFD0;XFimH zTcrv_K-#XVwh|hxoG?B(9@MwZn4yS)g6Eb7F28(qad`p8@ko3hQGWiQ^9Nm+{(eFF zTjAjJ*S2pL3>qwxdk3up_I|VRJ6nG9Q~lHMi9`xN{7oExavZ3wl;1kxUy3?KuWBhe z^5l=AO8rI}C3rtK$N2uCpSvSRtJUZe(1@n__rzCdMDGF6r^{1^hMW_19-p{iO0i;<=}v`bp(|#6Orq^3%yHiGRR_@0+LOfw<62+mFYE zr(RP(`7S)Il=>-f;Zr#(%Zglhww?O%x$x9Q>c?>5DO2jF%!NMR@U!b9=tpK&gH z*ECksCb{s(xbQVDymO=_rL$dlI70d-?7|yA4HrJ&g)ejA`@8UCUHEfc_;D`$xi0)97k+>X zU*p0Lbm3>a@Pl0VunW(<+SJcN7rr2c#OGoczR-nV;=-Tr!Y_5Y>ls0x?4 z@E5x1>s%w2|!h2l!5iWeb3t#NQ7r5|OxbQ_T{74tx z=fWEx`BtDnA@MB!~XI*%8LqHWSbK!sQqOWt|uXo`aUHGvs{5lu@ z1{Z#V3xA^vzr}^W$%WtH!r$z|TQ2-9F8p2>{#FxbPkqeu4|1@4{EQ@C7b>l?z|w!cTPJeJ=bY7v6B;t6lgq7k;t}Kh}kx z;=+$};itOrlU(>|E_{s(Ki!3&?ZVf%@L?DJ4=(&d7rxeoU+luqaN(D@@Ie=TsS6)+ z;h%NkXS(pqT=-coe4PtF+l6m*;qP$a*SYX>T=)$x{9G4)iwl333%|pKpXb6`F8q8K zey-|fQZy6_PfKF@`Zy6}Bn_?QdtapCWA;qzVidtLYf7k;4& zZ$!S$HR3sIEstiz8iP4jMXsh9ku|xiT}1fWdg03;X+855{MJwP;TvHY8TO7={G9n5 z;Vh8a8w7rma0kMT0zXQ)BjIHNKS(&6@KS;ABg`d;y;$J833nnK7WhuWoe9?nTuT_j zYW6sRZztS^aGAij5$4ju_6d9=VJ<1`0)ek3%%z0w5%@~NTte7+0$)y;O9xvM_!7cg zGT3{61>(%}33I7n?-2ML!dxQQ8w5U`a1X+b0-s2jO9gwGz&!|aiC`}kxGP~U4eZ4N zcOc9qfgKk3&^Lg&6tHUq{(&%;0QNY6cM)dlZd4DlL_YuyoxYWdRr4XNtmg;z4sUP|7F4+!aD?hj__%OHwgSB;nN8>3j8SHGYBse z_(8&6!b=6dk1$hnd$GWG6F!S@Sl~MepG~+%;9A1@gvSYdJK_F>%LKlSFjH>ZC-9Ag znQGew0$)p*DYoqq_)5Y|t?fL4FDJ~D+SUZVgfLTSd+*O;f5J?m?HvN2LztCp5Bw?nq_TIx{ zf5JY(I|P1?u%GY-fuAINIpIcuA0<43@G^lPBwS2*slfLUE+M>F;JXQ5K{zb%orFgc zt`WGFut9j7z_$|)5H1t=Ho{CrZJ)q55@rf&7YKYUVWyt8N8l?7Gv&1N1iqXwQ%zeF z_!7cQG3~vF#QubtTG~4VK8G+X*UXdB4MVG_A-Hc5N7IVFBP~eVWy1s zVu3plW~yk11wQl@V5W$6jle$;W@>1U6L=S4ri6Bxz&i;u6|{W&Wi6266Sp1`XJGu5*-fs=%p;@NxuEA}Tmj_?kFpCimk&)y*LlZ0<0+$iv)gewRy z6Zk>G69_LA_&&mwgcl2ZH{mM6VS(=?JdtpXz_o-q0omgOzMXJ2;WB}5BYZnypTIW~ zo=mtv;A;spC9^#OUrCs$n4Kr^<%F4n*_yzY5N7IS?>#8?C(M+~-XZWggqdpD8w5U` zFjFkMQQ#8^Gqtjp3EYD)Q!0C@z+DM5m9iHL+<`DtC_60hp)Ub5b+T&&{(&%4CVQN~ zy9hH?vdaYCN%&5}K7l_W%#_J45O@<|rb@O);5P~1ML193RfL%u*_yyf!c2+my$8hp zgu{e)2>cx31%x*U{3PMK2{#J-DB%d(K!S~4F4)Z$W`x5x+P4<)cecfo}HGL-- z@$54>Ll|bUH@C3Si0uzuYW|WK_6u;D;x>HWE@VCxt=3zpR=5c{kYVP01YB@pbIxjf z^2Z$b4!*sd)iT;JzcI{x*1L!Z@!`(`8SnlwekVq@K62o{t?Q9V{S{#3P^(_N(WuyD zB)V-d;#thBjxn;ffd9~J@BHqq&4zix%UR~|Di9m-pf`7dalv2_b>Rl&VDCZU$ie#F zbjxc0nCtxJ`=iaD%)^$!IMf)rpd|WbaFBHgv=}e4zJQ|qre%H{FjwAmi~r{2NpJg9 z-&S;l=!K2;H3;7Y4~+^);{C; z=3+1PuiPuPb3!@rna&BT%_-V>ngx0cvd4jDH(-}tn2#BzQ zux>&xOXFuWaE2~5zp>7x4ijb6y^)yfEh^PZc9xoFmV(~WVe2?>R;T@~t=1pTLSth0 z11Qv^yWwUfg(WBmbUfNSc}2S6CH{E>qf>i5R+ zz16$6<_n+Z7td;W2hQ*e7&iH`vs$Q@v$d9b;FwpUIXyZSRl-GR@M?U4*R9{{(OUju z#Ct!BFaEl7Hu<#rziosui_r86&_KJ^7>J(4_pF9u7xq$Mjl>x5BqI^@j$?lZTlf9c z+FBeh_D+hIddD@41$BIkcW{bNH`a&`fA|}yWATIRFr0Uh*bu(hO?PS$O6#zbLbz+7idN9XCnfmjsUzep;MWJPw-hz#!uVwmtoj49A|0#vH(u*mB} zGVZU-%EM^$^cl}FFbNgmoSUp;QAu1b+p9-c0%^pDd4 zm(`8LL*9bpG|h@#q-n86zqv9PLgy|*HFWMD@VC;$BB6j(rzxpA;3N4cbYxGW4!8D2 zqod1`lOSX8vyn^_D?UX!2VQ&-m~dm{^UF>!w@m0N9j7_v^8L zWwS3i)zVHkufDu2+lZ|OmyaUDxm`y?w zEEWqlIn|O~9l~Cv=7*)`7;moS!N`j>!nN~e_cb^IOEL0tS-F>tlf4{L@$M_X_26Up zwU`%^n;}Zd$u=Uei%p-!YR|p^7HnX*R@_5z*C1jcc*M@8VViUQ2p+`S^DzwoUI6%q z^?*xJZr%HX;>2#BRS!jJ4b31{-MSHLJJF%^3Ikm@Vxj~YofUp71ZQi()0K~m@t&=i z>~)y*vU-DwbSy}=JE&SM=yui2;jdIL$FP~+Xhxbic@LA%ZeFl|5^{uR=fE}f zPMVr=nsP1$5&@tGLJ;Plvr)L@7>+~C3>bjHD;nNL_A8wBfs@2KClcoCnr0n~o+g)W zfL<%si5U=2kyQKz;xV%7C}!)WiPyacp=1Mj$6kEOz%!Z^P;-NiVc0OUz1AQ&M2e5~ zIk?F3lC0h*fF>nk(>@KH?^%)rU?A}*+$1?DxXy+n@y9w38lr+_%DWX6poYIfPvT4G zi4@ixTU6}@mt@^hRQ*>(MB#w&nuqmcPcDO1)l;B(DZ(t>iwzRpH(+jumkmHO)_eCd zi^FK0q{l`>uhuDO1xprlwH`eLahO0{(}iNC#ZPHPf+Am|8X$H4P>44_7mOYl4-^Jro;L4TCPYZHfUG7GBw5j|del$Pfx`Nh znup;910t=h!S2Nz6ssI#0$UJkou`o9q2Xd!I6Qpc7an?2zVf8Aw9wU#L#;y6ly;~~ z#G$NbAf9q2J$f6;E%t{_P_a(jNqhi~bQP3O8#kettEn2R8>EnLVt<+c6v)n6FM?iy zbQSCGX%JFjeI;PfTZ280R_pw4s9e9L7!#q6 z1mCk0@$Pr;R0D6xP7J&;5-D>o0;l9yAjmFsp1-~XE!bxu9u>mTo2)OvI|}2+WHQX@ zoND;PWGjr>E})%+)>;}0_pi_}Ra*aIn;1e0Orve?{JxS!hrpU%hf#9V0~ zFHviF4w@D3`bShPc%106fU?GOz5|aoi-aL=^5mmf( z=nwX^#6f;(>&isYeX1*5v|4}viqnDEY8FN+1;h4FnEM?YJ=$sDfPJTiaX7-O_dvjX zPg}cD16y3-NMja%6`CE@muJdiATRf@{GwN1Qrm@w^| zF&Yb1B#$5#IIZZhMc@JzHqurk3k(!mi06kjza=YjvgqD(#6b(k!j}XM^>LLUY zM?Wht7L`4UG2+tAy(g>NoN=k<`dcMmO8of_T#{8si9eNSoeB}#_Ep53uOQL2N}?}5 zRMx3Q?UZ%Og(Bx5C?u#b8$L0)`16z!DIH|bb(n)$j$-y_FiX~@=n5O9+2g>CY-Ar9 zc6kD{)RrE73F(h@_ZQNIbEVBv?^vb?j~_Yu{suMJcY|CwQa;&n>Ij!+@E!q|u+{_X z43Dv(v~@|Ya?VdR7ds17j7pOV*B9V-#hI zyxIzZ0aS87sKtTKEsK|^U9M!_7e0*8kJZMPa$e*MS{W4o07B-CH(+S}9`&q0_5$1Z z@cTaV$bL|crJaG*F{kCGoUeh*vi|>6e=|B!F>#4g}UhfeY4>XGgI z8Xwx=;U_}FV5KTO`UDXE$j@2+dJopdb$#DNZ?E-Vk8R0yfr|b9x+W;E)ZD|pJw(g( z7#aS^nk-6SHSU1YDmM80H{BY@`k=fe4h@!~qoTn9Rv~&D&o|7~*qi(Sno35FGB8RJ zEzNVS9MTq9FMgdz@8cJ`6Yxn{cEnRuVY=~pTedCxNM7wl^9^!+y7>NO& zfhA0hDE}kpo!C}kr^j9-sgykyTmYU0M45O`wVF zn4iqu>BJp#C7;iVS~-ZRpXZSTZ!_+zBAdehA-P9WFMxe)~82k;*?~VJu@E z3hDZT?!t<;LJlA+=e-Tj_WJq-oicEJ*1prKt&Ix6w7l;yA^(jkB2*P+rypji}8C6_j&QU9dls zvGE|tjyi<8fVFgSv&6IBlv4#z8UbN(ENkw-Y;a@%eGhJLPp73EZ*6#IBiz0Sa-r7? zz$aCZWU!Gj5Feax9S;KgY_4FPA(z~d?ynkWR*-FJSUHWt2wT3YVML57@p>J!Xl!>X~<&`XJ38~b6i=9&Y9x|rgZUyu66?}sqSa9bz;0tP@z{d0#Q^1zR2qW{gnDUa-?KvNhFk1AQKwCwc<<$?T=yk<8#&Zz7VgYtf8cq`r|omvF~k^E{SE zv0%C2TV9iEchou-2szI-OsuL6oH}6sFZFs#F^v`I(fbf!{gMCX=nwP;p^=#4MHW>b zhMEKBkA}HN)@;~TfGKd0sz*0st_&9q)T7+?FyOm+4S&V9Z!uW*wQ7~KOSV%CH|?uO zJA$h};m<QNs)4a|zz4vjX3zT&RFub~eTCi48#C&ClyPVm71lFCsy zk&m^8PPI=&tu}iFBF%Y$_+=>4qX?;}y3Jbm5kq0*QYP6 z9=)5X5K4-+aKU7cf{_+8)lprD4Tt(&;2?l4JDNIZ_Ud0bbfXY}+V9w$vuC|W9>S9( z6QV}118r=R9=i#j{`mE|kwaO*3v0(l4t3C@pP*LcP)9wAja{vN8GJpd<6FZa41pC@ zZ)3PykSE2T2hRrXH$a7_(L(5wff$Vp0d>e4-ccm%IsmaEAI#ykeXTVqbFM@UxYpC? zT&Y>(3B)@?81tHJ!}RCr(MwTDzx(eZQU^WO2Ov~b6foa~dp(?GN1a#STPs2Y$e`=*3*#|)ix6=3VMY9J&efp_kewX3RVe>mEhf%#RI)J^o zckR!yFlzHpj-+7>?FFUg*QE1MWf;ZVtt_snIBNDoR@vNGND<7HE#2Gd8|FqMehvI* zQ|QN1^A8@JPTKQPQ5mWV64Sf@z{IIP z1CQ&}zd7e$s044+?Ea!*xN~#PBfv4Eqc3-IR(;s)qvx(8y3M;0Sv=P1;>9iZDg8lE z&ED-Wqs#XMx8|G@{N_(EmyUng2cn2zx02ZIQgbg3IiPUyAuCEXqv1hK*l+T&KlsSy zhtcfI<0=cEeI3}tPrExR%Sdd_+1BL2Gp|bY|7%Q6g90(~He*0h(R~hcF?hi0d??N7 zp*Bu>>=Y>2?uTDi4kTA5iK7{1l8z9{dI1Ghi`(pp`xnhQSAz+JYqfvH#z*`4Q0{4C z5(&vVC`Os%otkq_c4`=js~F9LC8_Zkzvk!IreJ?;(y95-7;<@D@I)hit+>qh%{gDJ zRk2C#dRHNytjsgw{=HxlTHTzp7Nz`wb6|d2UB86GzYE5u>cR=^g8e@FCIx(npsQ~i zP~gm6yj{)^{3Uona>M*dkDftqj%^BW{!^EYa%dcr8spW`}ylN96j`xR)i4H0P{ZqghiQcL{W{KQw1gIf`QcFKhp)p>4IuT*89^1Bhm$(Sr9^jnulVoOb&t*p;b)k z+)KUKz=bXBzYZ?k+R`gAA`4Tbv2q8}5WG8%_N`yj~7*MG=m4E8{j$#Kk0Rc_I9>(c#^3GMi{{*=Z?pox!@ z6Cs?7-GyYg82RE&#pVv@I@D^#>-jXVF{op`wGm2_MxIL-_*w8W3OL@r%~dOv8uZE< zg_b#fab?9c#nR$^`J$Ou{wQKMcm1mp7N!OG!3e2-sx9J}ZZ$W_;+|rb@jGT_h z{}DCl-+GkIIX(`%m-mfrGW0-sp7{Zy05${SLot6^-@PM_aC)L7i&C5)+dc0rEIZ*j zh>BZ7A3NR+yTx|jVFTT_rBUg_Tr1GahPj2~dv(3%EBFN#$IcbWP8@Zu(5xz$l`hx^ zBUxXgz|m{0b#(BS_{eVXb%f9nLXU!lRxd~jXdIy&tCNC!3Oe{U z%FOLI*?aIMHLetG=>3+D>K9WqKBA&-Zw{wu6DXP#@I45s;2IYE4h3%iL3@3}!`H5Z z&O=@8f$utT#NJ$qJ=XeyZ@`+Fj^Z$57dX(L()Ol=s2_9c-vy&f9I|^>u)c@&PimBz>A@^WM-z$8lf%a&3s=m26#L1$Vo;LTjtZ4Gs88LH+9vjt;> z1l_D9Z$k2Brf-~9IWePFgvj`01iS6lEPltL^i9qbw*v??m!L}^hlQN!uYN4M^Lb+6wU->kFI0VjiSt!Gm?!ADTHjBIS|zAWEWPLSYt^A{_CYzvCkquhXX`maZa z15km}1Hnq;v-?_k97Vjqi{ovs=a`zk4T&%V7-mOnC5KgfXn+e0!yJJSJ0cHMbP2;8 z-4|AcHO~vk`LrJ0EG3(QSa{r+7cg%|9dsUsbfP^jzcMfJhv(yj^$w-j%hscXBNclN zkx=UC^0~)4k=w$0&-6#5LyR zS?oWJ6>5gZB2C?6aY>xA{Ax8{)7V3pNu3lPK6f<*??MXOLoWPsU>xFBBSoK=7QKNw zX@Id*!CmPB(aF;&kbWOA{Uog&HVPXLO>$N8th730I&$ona&zT1xMj*wct2k4M8p)=&Bh-c-TD0w<~1o6>kM zXAyu>{BNM2n!QIh;KKN-VLMZv6HevNfYC4e9ib@ZhGFngxT5{KATYnkL9_j@RC(bh zxhSi;`6Ie%9a_bn>(Ns<)+O2hh3GGUMh<+!*2otf?G7kYdMn)I$aE7pTGQ6RgO)N- zJqAHljBT_~yEnX2Qb2)F+n+t^FzaDbnKDG58L<*Q| z3LB9j>Cvr_vYz{{pfkVWUbp!bj&61~v|_YyAa6ih7}{}cI-|WxYj}%wfVX=^pRBuAGIrBBE%~!;P7-18rshXj(i#_SW8$w0_9A{hQhA0 zW;f!mh!)_`gVpNn0<6xcC{a5ZShuof7u2k;p%MCENGKzBeh2C*K&dqu{9NmU^jSmS z|3l+5pj7l-*7aAVwSNr*ds_Rwfj?6FIAoyyQ-Qw>?Th|DZPR~n8}z0}pA{qEEPz|R z)~m29_LSoVcsR+lTD;H`N99tKrL-AYiR*(w50yTbN}sjA_*M+hE$Q-AC=cyGwdftt z2i6^ZjEeObeF)tcHQjN8bdz-|Tvs{wg~~M~l?;GB00)Q5h=bn&jRlm0%Z{(OMd5li zzG9nTF+JKFby^;UoPb&$q}?KoX}i6ivYQG-As_5UbvqMt{W#i+PZB9x$+(EFgb}6S zhID}#aWV?5XTaQviF&Q1Lv>6FAGBk$~8WgwSuxTyLknfK)<8!ud{lZ`n*Y+2W zg)ug}=+@92TS|s}6C6k}0u?{`alDgtQ)w2i=^cPE(rLDB!cEY*D8!(>QwfYbf3~=( z1PZRJ_fcgl)0&YOb`6xWSh5oPnKxQn%dBj;QEEO!7=xDZaHzh#vtb5tkA6Ui`9c62 zO@4iOws+Wo;65IW*l0A6DE4Nx^x}979~xjJj4VAWm#Xk!-10n(Ydc^z^D)8Qgj|sqf0b$lZWZT^0*j7EZ0=!mu@XPuDR)gzfNvB7@0Rr=Fb~ZjS?PUiNb3enS zqyC@p!O@jFJ6d%sKwNRNH+z&hVIVZHHaE~*JP_2FX&^o%@152U&buf-kij2uz&1FL z7@5`5r)_;H^4DYq%W;URWZyl?{2AigSHfk*st9()-rQ20OySYfVso!+a>V{l_DNh$ zRqVw%PPVb~>yB1ZbtQN)I%5w56N!)Td+r6A(DvZWx3B$pP;?EN2b2Az?h*E1BSmAtg?8ql5hz zSiuN z>R^Abu*WFuC6&ar@Z4G7UDFU-qGD^-7nX))vicn&N%R#Lhi4NcO&aO7Eoa$+YQcQR7V)0 z#X&b%+8%?`W2dQxwP?qhD*SOn(b|l0h$_LeU)#}Zyq1n1+$6$c=4q5NGy07F6Iq=X z`XZ$$B z#OO{+`}aF~TJ;rjmp2eJRKArMovq66b;_Sax#&5z1Vk+jYW|}mFRWL>?*!gD2|d9^ z${GmEZGute=l(iA%U0GhhjBVSI@dl+f>xA@)2!EC#X^jIwRNCzUNFZetE79uOonR` zXL9x(fKqbhsb>}+cD%7G<$fPMvzCGtnhrQk`|D z%UfH=N`So4ehN)k?*OP@_7x_#7ZKM~K5!BmvV!QND&SMHsz5e^Z$g24K36wK3dBO` z*C0g2WY`_Li_T0#uR)Iu+}6B~cJpg2UIcCFRv)^a=PH`zL8Ht~w1NQ|#OaS6HV&pRhnPrkA5G zt}lfrwk)9^s0qT{CjJ8pWAA!ucvnl_? z2)qG};(&oxy9Z6^%SU9Fnibg6Joz$|L>m|8RD+=&<=DY}u{!}I_rmj(RGpMm#bo?f zVH^eq#>mM^cV-SE!W@`tCntb* z#NK_9Ek`Q<7BtnAb3LmHSE+4aP^I!o=7XcmcbR4c%wL!|{}eFamkq@~$k>E#R`7LV z*30lE87d}Gd>RJh#QGFUD2)$aixvb|YR>8FkBsb=W$lEU1S0bbvPPZI7>MlnIS^?) zv(#)fPO}0-V7jNkMQt9>RsDSsoc!FPRNzw_UgmbSyYV#+8z{OuoO0g1kn*Ph+I|f| zKq+C1)&GCR={KS4$-Ix@+DB|12hjbNmr`*TmMs8LJC{8h6m}K5&+}5;I>e%Fl-X&) z&K_7e`wy(q(3oN3ngAd8ZAQa%A6)8kz6U!tb;Pm?Lun7Lf1k7qyf`|Py<|Oa?la;S zG5NsRKN4|FFu5)LRJk=7E`iZ?3XwS9JxH?CeNkp^!JSrjC;~%sXWOy0(+}rlPu^dW z&KZqC9hfurl2~Dw4|$7#z(SH6{=>X_bjFF491kLWjhLNE$L9ix;{=?tU~lZM6L_>T zE4Mxmom|0v(9(EKU!;7|FPK%LZo^ zZt;ib;ZbpJzw5DkP#NK*9Fd$42ON*E=nr3tQY;4^WkicLzSiMApB6ktCW85|+i7FAm6RD2YrfT{P~%DS^$rKh_~11ycZO0(Ui*Rk{&SLxTEiyl-*#-SAA z2CuQ+1NybkK!>i>t>R}&?|N)A)GE7iOjzg#>4e3!^w`~)KQN8ZVO+}{Yo2OHk3H54 z0}sv!{EF+H(+i_ZaK0K6(l?cECY#q&&TL>jvKZi z4dIY1vMzrSmVv1PIJ>ZZ0)VJoh+D)Jx$$ugFLjpghxk*Qg z`Z;1%%YOp6^B9QF_cYuM4uy@_FSoXw4`1LZq~PxpytNug?AXnHi5%y95JejPqDuY; zC4oe2sq_|%o(3Al?8P|b7x%H4HNdf`;U!fkBz5lP8HF^mOzPlC6I4W<-%Fj5dDz}B z=U^ePTs_LauxuovULKy(X_w)%)WogfQWHDprPwBcToUvfzlCm${u>Auw4B*CfBWU4 z4!Ggr1uVEzoL}ISfjJ_#AxTM)lZ|*mBKdn93~UEarsbZ{0rzFZEM$Pu!U6-a*s455 zM#>YsvWqbyS7f(atA?#wIK33ym>%g z>|*`xUlK^TiCt6*wu*NXyM4e~D(rDp0fMJlA6vMIWrKxv=+dT+coYc5Dxv!o{y;FF zNxo5H?npkH_6p3M=!=0PWUF}@d}mXN)=1334V9W)t45qK^a6MmV{@0CFTv=)|JQD) znRv{*29X{9(~^&Us>m1g|Mze%f+fDxS!(8D{=bSVa0@Vz_`F9SO=*Q!)-qQM&3mc@LNmebBIN zh>f@Gz=E~>Q|&`0|A?1e;7duopSJ{2cgLv28+-R)rL+;w!^y4_>SDM)*zlEu`00Pd zn_lJ87(rPJ{VUbpDAV@WeF8E$t5Y)u7hF~?sz6FCnWZE4Yy=hD*B|{OAAw&Aq4iiCbbu)`zKKgReL%$XjgWut&I6 znQaW&8|)t$k`?+kT*uB#@T-Wf7jN+E#fRDvhCcG)^)pevkQMt{%v_d27(L3QS^O*h zyPq3&$w->*fkWLh2#?LiNB>;HZSh?kny`2oPY77AaJ_0NK0D7UeO3*bwIHIyd-!6S zd=$LR%ec!3^$4qVw&a{tY&*8c(-t9??BFtq37p0L}RfrN&k*j#0H=Lit8k0&e z*lhC?>px(WLKaygAr>Pvj;(Q|r{8lq+JqVbcf`F`{ocudok{prhH$K*{B_!=^m)Wx zyeiGdc;s=MA1G|wAnbZhM7*8+3^js7Oe3*KFiA#ev4nN?C-SJ@jW_7A#}6D7@kc`EwSX1KyeHKTut*lGU}`&X%ThPr~M$2P!)!Kf?rI2f{0CI0yFjz+9E z&c(dj5hGC?7()A?kv8vr9Kuq+-yf?SDPQVghVaDmVEOr|%_%L*{`wG-+J)(yfYZ5k z4Mb|nx$+4Qni$j89lqs~-Qi9A+~hjAA+O^Xwd<19KaO2$_zB|%+sloZs}MC2NVKFM zzpy;P)`&^~C8PKgLKHj41Fs@6x3%LOWohf5PeZSGO-~mX)uTsq@yKOj>|Ro14)-+h zb_06zl0%NSMeLWYFYE6Zyb@6FI)?p+$tL0U)~4XetO}b(*5)=`jmp+cR5jw)Vezyn z^o96Q;uUWcVp^rpAg*=tJ1aHZs@f`X+T!CIbFGQ!DI}RI6jh4ij5Nh7-Y_{`i}(pn z{4R7gitccK)k%hhg`~R^odq4g3*DWHZnKMS5$Sq>js&13+;E4Ydp=F~koO_r$#*g7 zcS0EsJ>iD1qMsx5!iMAx4=D5mhfSL@&qpUChp|vytKaiD49rIrdW4wFc7Sg#0)~+) zVs(!k%+c?i2}|R>j=nE^9BCavacox%Iys#$-1>%0h4GmMlC2+>Fo$L310Skj%QtPR z!4q4HuV~f(2Cpdi^@rEsa)cgRivi5OVD#^5xPG%wvtG$3W}YGYPn<%;Uqn2--6^_< zUHjj6LAX`k%%6AgiFKslVoNL4zzka1v?a3uK}jJkBVP`_j;{N&O~d< z*U@9T-8p24{qtIQ3T3j)vz2g2D6CQZUhzJ6`XG}ub|M&({(SJqoSa$>>_xsq^lM<# z0J$j317Ho3fz~}wiY(_HAWp~>jCr%30NcnK4;MGd!BVown}-tVlr{DP3472o zgDGgt&#VbBrM>cdi17w5os{UytFm9?%ps;1Bld495j+;W#B2wEyW+muQkJBP_Rx_6F{F^Wp2*yPOnNJYJ2j|3^HwZh!^s z$3Z0Pu{Q=nBsr%;b^7tCFwT)4{A7;mxb@Cv<-z=O@zQ7ZG&l>>>t@uqDc@gLqW z!$eWIzHE}5_L-k>N~L`D_$JV=1^5fT*4W2HRHL z1*gKTLYTte+?jd%Xn)TU&1}r^9f+v~3#Ag%ZVRI)rzO zmW{9fm(l+54|H}rrY2|pL^V9n7OiH67MYjND~H^PxIr?u`S}tixN5bI@v>XL_h9G( zi|Y>O!E_COgp)(px1vz$ZQ)Rf-vt0^=34;Ntsqod}cl|R$PD-{at`K`sfvwhNzSVHvdflbPe9A{Schw=89#5jI5Xdv$Wwy6hXOJ zXTYB~HT(*QYn0310|O*0(Z9=JD&sCdenTf^h!0M8q&}WfA2^k|#ua0FM8iTPZlvYu z^(WTzDN5leTJ;q{O;9#d`^0 z>kqkAqp3irN)gLDmlRj8E?3U@&po>Qn71(lW?@98;6N&g~fNx*|1oVsAMp(ozx zOZ`5MyVBMyl&7Bg*pX6KLnC-n(!NmGb;4m@k$N52y_^rYSAmP;XF2|4J?JVL!A{h^m;&UY2JTy^Ii*uNJaKybO^$Enu*T;IPf2;S z(%fM8K@IE0N2S5*k7)3ZM>g1mNNI0F7i0+YwHbC1W&plr3<2GmbrST=+c6{a_1Jj` zagJ1#->Qxz^WpYwDwN$itCl=Z-Ib0Imx8OfgoLw)`q3|)7;vuusTgnvAPI{5(30JN zU!1J)#@_B1KkU(x^6Kpu)et`gu5Z5veS@*5;d+JaLgZh5MEAGD8R{p%aKiB#t8&77sqh&dpgSj{Ic$TTau>p zYYliizw?aN?HEbJ*Z+mFG&9iOk0rhX}E;AgE)pQezz$2jh}zkC)f+{& z?)ejj2kdwM;IyzkMJ_$|HB9PQmw&&B#{oA%R75@dQOIv3hIMT4gERo@@HKvP>$4P126UOOl;<-Mm5dSQQx?&IdJ+Rtsrz2vlC#r`lkco&-o z#1X7z;E3m8`=GXhgQ)QJpP#c^xjlHx>8too6hOZ8M|)D*QbZ1N3IIsDx%lpfSwBUTec2(^@XU ztW1^mMIRh%p6{R%M8$h~xqq6=w{ZUVe2Og|d#pplcWyY<4kNCU)9+EuANe*5ueyWZ z^5HQU1`iFuW=TxwTKy^bRfjHV#r=0eZIW&vKkCvvWTU!UWA5F&BVuRi>sogC^;bLD zGr^;N9moDZ(qsQuj{SA8gVhngFdo!J6W%?>lgqdV5&8Bv59L?~KV^?Dd})zK%=ocL z96VD!#f6;%KEu+@8Q**oAFPQ%ekP?UoBC=WvmJ|&p;ix3Xh%dM!ZO&Sv?er7h+Vwzm#9$ z9#K7-mist3YyCL680kP;s)p$MCZcak`uQj9QX+dV(MBbnUJB;)bKLWTKr7HObbkzQ zBJVWJY_Iji1I!TFk&7R~tY<|z8G|5{2VAzq>vxSQNwc>(jfe6--fddjN2pSt7Bei)LmLv|6` zuufJIb%I1p}9+p(?ESL-Rt4m71f|)$kW>3IyvQAz6oB53! zamb`UGd+-V__PI{-~17lUEo&dV?U5mtZvIsE_P-+0~5X>Z*U47+k)rlR>SP-L1v6- z>*oX$m5_y3cd$OQk;N8<;2?N!!)4$2v=vw3msJAERKa=a0?9@OpdiEZ4-39EdL1zz z7XjhS$N%gOiRGOedbAPWF(Hd{s#EM7u$7IaJXVmY1-J4Cm72{6=7o*8&s1f0)}uSS zao=ivN#UkZ=2|{GfZGa-xsBO!g_CD3_-`x6;p2@(TW01O6)SNZzox*yaH@LjEr5el zR^p>(U=Oyq{{vM9hJ2vk`x3t3uULd$_l1AKBX_5R55AqQ-miskc=~Rd!u8jqz3{Ch zx?Yd*i%(zvlW%`hSdYeV(6C}5G{}*NlmO$S7~k^nMzGxCg?K^U&2SeuI-(K|P*C73 zoj|Ndm!dLGB#ZUs{NogNcZFOO6v9*Pl)$m8)u4?+Tu>y!?0;a$T0OD>LzYiudGMLI zAD+?q_@q7Zl<*B$7sQ3t?yb-PZN^%yM|PqT3?}O+v6_7y>VRM96+fOkYNK@<7G2Dj8qx^=Rz-UMZ#G|O zMdNa*_cA^Wp4ugOHc)Cd@rfI2PaIb^haA@LKLeT%R5YWzH`_X`Z$aXz3hsLaEhR3&S;?Gb- z_|PCdItmsE9~!DhOJOIpsmD5k>q3d<2<2a6lF^q-*ut|xWm#zT3sfy-5JRJQHV(Z? zHTqu+ap~1Ls?qjM#-+)jdhAIWLB00tFZ>FTrPf014X1SxF3QJw8;Xk)Q`w^xjVIuy zUiEI?o)8tz#e9iselt3t8y)Tb-+>s_;au(8jQ}4$G+d9J!^Tv*r}As6UH@;|-GO4Z zxuY-syc+GrH#Pec*WuxwO-A)5%AlI=l_yRAKp#KSiZ@BS(CnocCWt2ui!eG%IX`~C zkfD$|#9y!h12z>#Q`WOcAmoj$^Wo}jk8kot`>NTS(F&S15)btW!>jye@k71z`>T)E zb}Q=Bys?YONcH(#eoggxn!A?hx*Nm|R5U>rY0+GTCz^0uVwFEp!fd*_k-f%-eg_|N zCNVo6f6DG{WiN7!p6_Ova^*oH2$avu?~Wi(@@q%y-6AvvV#$@+S4XRS}qf!&^p9NQWnCk&(xHOKx;ZwcdG1f@)m2SCW ztk0yQM+`aINMHWL3iBJ*ZqD+(+{X3Tx`T>XC2Eg^Lh>xEGosMy^APu1!$hvDxqN?$R; z7xC{PMh;?*x)O$o9L&a_X5bgAE7qF&Js0A;-yGmY?sp!(wyh`A7U&5yECBZH#|@v= zl5z6`)aG>h8-7ihuFsKTMrWfK?vk>KC?w@NfyC?CV^D5whE`>~UoVDkmNQ3bx{lpT zN5PX+WC~p&ZHF#Pw>wVNQ__lhws3etETtdxM`P?;^b&SD?a!U~?>Bebe?sD)y8k3| zDE}~}vu?$rl@n$z+{oGspGK60197U%rSXx=V0pXWSAyyC56}#g0nRA9(Hf_|(+@%f z&U<*il~Y3DR$B+krEj$VgR6bYv*-vo;DQm9L88Z2v2SVw4?$aLEnE*F+-4I+ya!)s zfLSzIg4kP->!te3_VU5Y4Sxml6buJ0hAV^Y|2gXH802K{qXQ05@pu<@>~wq!Btlu} zKGo3Q81kWh4}+VNCm2JT^vJW2Y{7q3Uho*c$1r6tM5oNR?2l1j=5cmVHYUV`-vMo( zf^LYv%Tc*>O$zyU*X%6mqfZpmfMBB4Ar3kAXE|4tJWrL$+McsNwf+%*!Q`b$`Y!wy zke+{MiS%`FKR5xdAWLqUUdPRbyu5%6tb3nE<{J97v0M8``zV~kLB#QzUWexPfL}{ zFn6CF-Y6saFVpq z`RH`_#^DgCh(8Cg6$7z@z3@kJ)ST(Jxs)^T^aY%O@we7603`5bd+mp%W#vaQgy>8n zST%Om?MGy+_++qO2$uH8_=QpaBR;^wzDBD1B>%<@Tx^>$ob+X4Tf7iqo% zKKme|8=Ok{OXSz?dh7*sf`Q;TlsNgS7y+}x3qaxh7L15wK&#lVIsk2JKj#2+2>Wji zKt;))e`X!JkR3sZ3_<%2exr%)=?*o0$iB@1=s5Ouq(PL>?BC&A{Zb1n!dlc(E5eFz zup+$RZmt>Zb5U+hgxwSo-)x|WW|HVw5Vgfxj$iD>w!Pnw8;HE1`#e62<7Z=_!zYAR z*YCB_?EqwJcpeH^u>!xWVeG0DT$wJ|%z`se0Qu0Jzpui3Ode~Dt_s!4%RkItMhC~C z6pmZR6a+HY6`_sdXieJ>`$3hTeO^8Q4^Ma?E=N6k4Scz?E9>EAI z(art-KkQTHqDQIccM2QP4_7_`eUA>}-96X>n4f2Dy9=E8X1ovZrM_Sn`uAeG8`8g< z!?2j^`Ny{Xggc%61pKjCyoK_Bb^3g4bd1g(vTA9+!l>zvpw`p&9AC;1Ywi0d3Aj{>br3nUv1Z=sobDQHd?h@>y0plWeu z`R;~RI*!Pth#ThSK}2~!o^u9+m8Dz0Dg-CUx6LKr7tqj=@@=3)>`RLw@|{chq~M2i zfyme62>GVs=%r#-Zv1)}^KjydEUE`m9ZL;3QWb03(2jQvFyf>0(6n#ifb)$Cs0d&a zUS#D~Ra^XM>OciQjX4M%_+O)9MlN`_yk5VIvYyQ0*yg+H zEs=lkA+vJ%2iivFnRvl{#r~%30=(b8tUiym{@K3Pw{zRe2V>whwAkG~j5n3;MT1eq z0sJ*qZpDn{{56g%m`TCmdjDc?joyEeHw+@Z|3h-AQ}3^ye$xB@Lp{mR{{?TOl)vKL zAfK;$ci_`?NQQGF?#Llm?gz=_#XpBY!8>AkK+v$*FU`e19AC20tpK0#b4#&1B)h-3 zLW@HbIe8dm?ns?hKur0Ay}A8=5vQm?@{02Wo>`^X0>A|z)FryW3c)jRx?v3Ypmrd( zBC|Gd=q)iY3uAlVN%BJDkl;}zjt_RpOmeNZsdDS_ zM>Hr20YO6`I724M1OlW9APN{JGiQ>4$;>!&CM4Eso`Fa%V!7`sO>b=X+>4yzK6WsP-?>9#zCr!B-bD4)Ew!BXH(9p}iBa?%gr6HseEDuz-g&t5 z{YO*X^k3xN>p!Ck=%{JNkeZHy^k~BR5BDRn9Vu^9ytAUMdFUtTOS$oNU;!$O4c%4l zV_<^ti#!@~C>WuxE!{U?BxzpAZ0MzOP#+c31$BwIsHTf! zG;M3=bdDp2Mrq|X^xnb5L_9#7SG)%s9LtaBL$41VZytJK;1{=|!2Qab&68e65gwX= zu^rLl&o|HgWwd_e0dQhII5qLE6I+_~Lw_pXdV}-MH1#XNuR8Iis(53jEKdGg%nsCHjR&9I^S6e^3W)#e?pp@fSbc%kY;Gtdvz()7xi z=FP4nXy7l9?Z*9ASsQq}Pf@LK)3uTy?w&0eSpm}n>*B#hxDYRGbCNOcAhITWLGy1WcP7In0rH-11{+3dZ z`<1iq%_i*TI$uf)`#Xpv~l?K+{WDuL$lA& zs1_sl+~xl5^j4rx482XG3YI8_o9l>U=p+rdH#3IYvtqd6j?(fY4!kFJ?(d-A&uYa3 zbQX&~oxO>kk!^z4uIO$YQjVr#*HOB6Ni~P$d#Pf0Vy<}510<@!=C@!C zG>=r#`xK|s8y2gZxA`!)fXclUTa82S;pr&U1h)t2_T3sRwQ#>{)wy%Lp;usrfh+ZYT{*lh|$k|3*+Hn9yCh|df?}fO&_A){7YO2mqCPG z8K+17L~R*3(wpU@`XLB+3u$DZN{(y;E#e{7U1*d!Z+!K|uXIv}e=PBqC2wt}M)nmo?FA{3SLo;rCtfzPry+MCqw6}PZP2^J(;lZlJuo!Vq zdz;?&N%K--@kSDatCT+Sel+qJDor&eHL%8s1EW`)H^H6#2JY-E z4qfG;_`enH#?$iI>VGEvp&mFJD71k%nEc(IkqLi?oV6b;=c&I!j#xssWy^UK1@3|Q z*WzJpGChn%d=lj-GztS5MZIt}zj*V*ghjU2gpZJa2zyKX0eeVTyl(zA2&KU-DBg4k z{AiP7A0Q0R>>3!IZaN1>Yw$X6AfbO?Qk%YoQgKLloNjqC&fNl|cIW-*+<>IT&7bOw zS4x!f^@vAI7HQ+AyF_f%BlRf5(=pSrUHC^-^k}}0o5Xn|zri)yT@x$B-|5AhcOcYn z+7`+i`8kFV1_`F1DWs60D@T4HVCl$WaDvYgAIwR#-bInE^3U+hxF1}S6?$&Ewi+4Y zelWRVZ1f_)#HP#@#hb3criEN@7H_J+HEFwwp5d)3APT%{M7%Ws=fvqi6Ga=aTwER) zgquzr3*ivTN^;m(MZ+cNI8E~`&NrpQCP>l~C-8I(8a_D>Pv+d~Z}J}D9px$E^*ZjQ zO@6Nba@^e7T082|wf&`UIi%0T5sZycK2E!s3->mNZhBHQBqKJ7!;8~yB$}5s^c`CK z7`)HJ^wvMQ-+)my6+ASj@W+^M;iC$o(rh-+(8OVG+B87(66>H*v;Jqoun|_UKJfC?59M?l& zgh!;!A;pbO_WVqiSHTK5;qsXp{GkK+-wm_)Phbemd>I~0$7%?105n_t@UwUW5Q6YE zUrp)sXz@d9sbg@00lU~Z#84X0wd8E2)yLLBtJ|SfdT?;M*ki64qX*=nrik8Ea zrpJ=W+wT+&{SHPEJtaQ%oE?5(D!y%9>^cHNovL2FK%Fo>0@VmTjl2Z>;?1YfeDX1m z0FL|!e+2e3UxM`${?u;H5igWUCMF~8uBE-G;s@@9N}1PY@FGK)0mDGYZ+?RM9JRpx zl_{Q1bkuot6o$g@?&OxB>UY6R>CoWw`&bkA&T$VbqUj*Y7(!2hZhz_T(nDxAh~YsF z!l}fu!54oHiSHqy=-=||8ji<7l3-^p?nul=a zUJUo4!|1SiK)r=J%WTO^HY_7?$7AS+t9!-00Sb{Uy5qHYXJM860D?N45D_?Wap}p zZJ^ty|K6ePrx*M`vjH7_Ac{YUuLKbozp+7;K9nlG=mnGxpcHVavYl ze*l*aWcq@OI7hu2cp3YDV!s53P=OsG?EAB^DGDw$_G84jpmD$$5xTy|n*? z>>@ow(r%J;MYg1a0gQ0A2l222l=j6AHnQ4Fh&jGpdN!l(FTy7%-E1e0bFw*xz;T}_ zxH(nuGFj&e6o~x^tGpXi@*cZg%6l(E-kllpA|$Ul#a0ZizjZ;LC|Hsz5b}PFf-HG- zdar5dZPR_gaRpAh?45khf!9G7Xiz_OFI}PUe{bL8qujWEhKh*in|c|0Era^ z^ri#Zcj9{rAtBb0vEQj~&Y;~o*31)l*9n_b&?B9r--^i};nx9F04EsU8M^FpkBZ<}#>lw_%^{6&2!4Hd|pN?cEj-c*%>gUjJt8h4@`1=R(ZXExi zEnlygcVL(MN`uR5r^y_h3P3v-w79hBfYaYs>8-kMo9Nfn?_pJRXoSv`-X`e}O9UE&>2lr1*2S(2aceF&+x}{JHw=iKjt?^P)b;CjSpa zRB;eb%+b-&>(tRv^tBBbdmEv35zMQ2hYmGhomH=?_bnGeLKD8$6X{W=;?jSSgcWOM_GrR*k`FI-ZTpc;x+Izr$0yCA->F8yy-R+xVBz23ss}j z6~8Ke1g9vm%`c9_PQ}g8=6Q3zwo->RJZIv3&7Tsd&?LMA$1EP13{tr+q$LG?XLGvw zE~3?d8w>4oAA^5Sy$|{oQJM7npihftQnX7ET)Mtuwtf-wGt|@$si~g47hRxw@+7K1 zLhqds@pS1Iq2kM;puwju?z@b5J_Bh;6mO=jKe11Y&E_c^CEJ2bw!1Uf-UqgbQ+`2# z7wZqoqjQCa#oRyV%PXLUAJSJcMOcdVAwq7#e$>$Mk^c=8Y@LmK71YQb*^G4JhaIB# z{iN=#Q~rcV0L|J0E_k)TlA*-@FJ?&31Jd4l)OTbZSfC!a1MI7!@g9M3%3*ya8RF;B z0g0Er6z^ipitjC9SM0t09|y5e(SgS?HzjsrBw<~#?`fK-j^NK~*5Akuau~bLBl}Jw zc>!eyULulOa7L@L=9BUR7X!&Xbk^!G5@E2x@K`O^rbA~L5ySm!IPs1WJRb$mf>9>@ zOnenaSUDC(kn~k7Surk}!D9afh*mS+fe8>s^Eo)2JGedZ8Hsl-;hoKa_eH`RP8@-v ziErw8sV!iWRpDNCaO-Fs(3%HZiK2g`Uo(yU6OsAMr=S&DjCtY1(1ID%|4)-*AS7EY7Ck5K_$SO=0U={FAVzlfNWF_TXTCg#)l(%`c^PKHk78EBAcV*e&F8STQ^ zL#$9u6RV&pDBzDV(7_5k$7SR9yZDDLy>@>P#mA^OH2+W|?lb-&@fTRZ{*a(L>%#H) zM}^`|ygzIm^ovh8K8M_aKZnhw1An%ea}$q3Pa`*B#PPm=;tGK$zAR7r1NPG$V3&vy zVxb(G@G}W7(l~c93ab|V*M>Azpa}Fd;ywvf=hn5_$Vp)A+fKIiDELU*nuuiL7wE~v zRQ$=jUzfhWyHI{(1qV>@1qtoK;r{qPC;l6);n(BmnfAnYWOD0-VSG%IcCO_YRN^nw z^hihBBP8}hO02lsxSk}YzLzcMdxTr>;U2f?auz=?a)wUA$sTg)_N|azgO)@68n*6di|ZbM(7S825arV&o~WnW zFpa1d2nLN#Pf#LjAtPo2SvSLB-DemPt=s4hNBeY7Fc|iF%s@D#`#phRJZeA%;wRd! zN6{#vlWmVj88r}0;VFCt?|Q)@F7IaeuMBrF+`}-z@EAiWq1_knjwn_Faz5G|@P*Je zRml0BhADG6?d>wWw17+fI6oNn_}YU;j}g>n#ADGJv5r7!M$8ELy3uUC!qh9mzPq#; zKBH$wC>{)I;fN@Uda1wtUTgPsQB|y&8=&*@GT(H)%r~t@FN>>AponK6c6x7+tWuc|v5Dn~1oHiq5F6^^pF7$fA+8d7goWbA{BNp>?8misYT+jqC5Di~l zFo5eAkV#-&xVzgE@@Xycj$pvs7WexDz0HA`skQaRg5gfB4L!5b6E$RQ@!&y>)YCxY zb2)}Q^y2cE9<3}^qkFr2foQojOiA6~@dkncvroH5AkbK!0S6K@*x>}IS7`J{4H`ar z3>lsv9FnOUy&kU_?9d3N5g`B70bgiKc=M$a%9zc z$6!H`WAa#^6-~Y|MNOfKaRs&-ej{pxyhdycSzEk=tU~1*y#aKMz7f`_$4rkIk7+d6 zQ(00mQNxT!L&#*58PEr;BcQFPpY#l`f{_VO zs6~Z^7*S4mjX)1%mj!)=S|H-_`D8&ilNE|l#byS754FhgK)eY@Td{QU(q*?TbwETH zI$ZN@2#`E@vqDr*vai5!(#m4<^jO4Si?Ud^8L3npkbyGte7bZwGF)tn!Q~wbJvtDsVQsI%kI|8 z%kG#adXKmiYB6aSs+Vf?hTCMplBOl~6)TNs463csXI54fYBMWus+_5%s#L*MS^{EzCzSZz(HPvIK3ZP)#g^Gl)mM zKx>g`(t{Yj3YXBl$Sl+fD=N@OWw8oqd!t6(6`{F~9fL@FgW;G_J`J^l#x1bqw z=2QvfShqjW9y3fBU$752epCUyOLU@$?9Ll8bClc_M{EKw7b4afWF!SsrB?(Y)c3@T z{`oES5_t$$Hk3Cnt6SV&ze@VD6#B*}25Ck+Ech#yDtx_1$Rx2=QzpYFGwuq<#)M>7 zAwLkY&TwGWEutuv?1^^9YD60{vsJ4|bfGq>ZAn8@`^x&(wx(rE^{U>oUgQM>9o@oc z^&V+PP=2NO9|q#B@drbZnwA=~CSKFCQk!9z-WjAHVJwvtJqVdn8N#B%7~rOdrjU5Z zBok8^t>lX`(t79+~c zgl`Q6Oi{B~2AVY!p29Fu?F^L63{i|qW*4V2;0L3aUNcHF(uEpSwIQP{oh;P6p3wEC zE=GbLffCHHF59ZD#>@_%vzBIYIoO!RYHwE{=o_0qcFrSj(Cz6BbjQ095(wkcu_8dY z(Cak}AKXApScUEl8lDikPL6UK&6;oyQKJ(9ih(F3(*fizf{Mu1kc~^OIf!x;LUm>r zMKbMZXCxZ-cEd2ar4d*kk~A-1)Y9sTAUOlk7K(Jr%VN{Ca+PFeSA{O&|HW)g>I~_5sM`)N=JOWRwRMl!tuH-zX9)$1J%ijxp%7j#2TyX2!QSb52=*nfr*S)PbG9yRTH1(dr?0ys9MrE5T%S$Pc!BN+f{g$e zr=x6I?xIh60$wAfI?_jLL*1O4X3bK1>Be}Gp>PUI_QU(&vFb8{;(5Bq>op>@{H7|A zoVZw{(BPEeHcjCwWRiij#7jB&b{R3XQ*>{%lkVGPIE6uBi8^VJN;!Hee1vmX3yh4& zPlrjAW%Q&V%dj(}*NmcXbcB2q$YI1IPNtgXW+%yWwognYunu1qFN9fWz? zHTwJN2hr>J)Z@s#D{e&l+Qpnoo*m&3M^?R#l&8m=A5aR|!#kK-WzN3%3lvh2}`7n}n zI6N*2+fi98T{o!7Vr*n*(q!PNsX7SV0`SGbz}@M6J&w@hwCg}k)mMk%kiwzZ+QLZ{ zPF?*b8PYL9d107}m=%FQ3vT#h{3d1329%9*zoOSCMdKmJ*Wqr+WDOZO0wtJ6!L|i? zJ~=m{D3mTTA=0wdh|Ld#7XcH95uE~oSj6y7qCWwDAND|Ll_&BILTO3Ylw%-70OYrt zTtcQG3!qcD=Cr`)mKn6)M!}2+5mKlVxom4-8`nkbND45qX^Pr+U@He3fYeUf3G@jB zVD_XN6A00uM|g{Hz5@#?9lKMJIMs4x8YzPP6*Cp^ry7f%r+%e>*&5F^m&*aE@KdvE z?m0a*&INA~EsA(T%oPSg1oVIylail|inG)VUjv*D16u&p3DBnC87HmjIYNK2YeGBR zE5zzRPSU=fKnzC7?Syvqu>bEQ=9b8qhz%F}YjWFf9_bBJZk%Ubsm~9_EzF3M$Bkvh zxP;wxV|kw57J>$`-A>!TJaVMyoME&DhZtG|YcDO^(vh{WhjGJe=z{u7MXX~&UzCrT zE3w{;VghI{+nce@*fP_|4w=PX8mL8`T23=Y#%3oJiQHA@IPZo|MUae zznjY+VfY|J5gYm2{eh5A&QzNGiFJa3ZN|)W1_smd(vjBE2eD?bPo;TZFztpaJWbeYe;JGMI94!0EfuJ1pa;sMOyTJhOT`vlWyeD zy-ZwLl^Zld;wH7fd(56^sy~@Sy9ij6Eou01f2(qDS2$xZjVDy$Zupj(cd?491qvRHOoqEzc^pwh!9IjrI%_D?M&ZW|WT6Ps%~fU*MQNuV&5F zGghy?wN{%iP04}#YS^zZD#Ovv1sa9JG)BB;ueHmTwbU;aQGfgL`nKim^-EVav@Tns zO)1o_x#k+ZrFB_jYkgZ=A-1w`GkcoI{PYTEt8?k{rls|?@k0ZrR&IvU(6sU=F0T6K z*1DGEHDc>kGIqLLnOUy-g)16yJ3($!32*5a5u|1GT54O@+*FT>v}if=={~Ww#9iOK zOypCPlbQ)Km-n=-E<@I=8wVV}(jJw%tC89-Arjh%Z)>kxMtWIM&&k&MMfG*dIpJz* zt8=!xIOVLXt8Y<7%j=i4G&q}@MSm;da-Ng(mbHR}sPN=w9Pa4QrdM5Sb3=s+p=3Zk z22VOTy4ir06_y+v|Bk_9_0Tzi4<(=ZUSD=Ytg^E5daQ23dJuOp3@JeI$6e+STnN>d zm!M+B9thEzCZk=l{o$;=7t%CYR9pS>*7}y_+hKs-u+I?or}w5W#f)M0nrxUtuSD0h zw9Q@bbk(=k=mB4EZJAFO=Px9^JSdSOA^bXbqr%TXRHU@fld`Gagb%aMfuGHo^dQ;> zUmL=$;as|<3bQ|;u}f=9YkIu-p73e1&7PFA`L(M_l;o&ihP-y45u}@Msz|`ha?p>N zv@^@odlG3DndRQD&iAFy6%=IBtNVs&?pgZ6I%SZU%yIuN%8?2*B$9ASoIQbkoX6?EO;Bv(mg1 zVWkrr{56Ox0=+dFcKfX5>XsgE?6DHK{@9u~+e2p9lc^iJ=JH^8qY?FDJ7Ju1xio~6 z6L((HbZEVd5A&|bpGV){ybWc;Hz$)N`IbT(4&$(0HR$#Nwg493OkY3o!I{bvfTeWI7VtP=$plR+$MM5;fO`Rl0TZ3c zBz-8T6lXZkQ$Ao7SP6gz^Em0FDCg0z6I+ z`T(8+)NtA}f#)W=0L$_4Nk8B);4Z*Xz~g{!JZnYIO>V(sb^U+^czkg;U@x9}PXJc; zCX?lpkOo`@=*F|f2LV?Bo&qfR4D#VT-2FI!3^;l}+5^}%fOZ$59-Gi^!0OFtH{hzB zsP|;l3vd^~uRspqsjs1Z(C_hG$z(6!D4@d*xnBo9;PCe$m(qC2TS+nG?t>lx-GC9m z1mG^fE&I_wfaOmolcgod2V4bM`UBtrx&gNU_5$t%+zYsu($ApYfQdtp`%mC=4E+sQ z`V+MO63_uU0DA#f0UihJ1+0D%?E>5dxEHYWr_c*vFW@NPDZtW?f&K*A2e=n-6=3yC zpa(2?4ebK#0z3m)dJ_G1Dfqt*d4ScYP;bCpfWv^LKLZ{h{TOI@ceiSJy(M{9Ouo2a zd)~z*ls^^!cHf>%-av@_%Z;yT*P@*8`*F6n1%0mBOB(E@i;9aj7W8YkUOw-Jo36c@ zSP;Dn|5n|ROkPeI{F@HA2LIL}-${85_L4!H(_T7|-)Psv_EP6zdx`U?omkVa8UG%{ zzkcBV3bw8-w3mF&w$NVsQ2s)@zHP!nd-;PGIqg-06P@dg2>?O9_3+)AUMQia_&{8(lhhA{A z8+fY$sXp7NJ`d(Q?fT#Z$Q-!HWv{Yz7TU{^gO*9@O+~h)$ZE*QYD9%*gGCU04&wCi z5U#a)d&z^Ok3r%$FyZb(dnr_e>NgfSab3qvnGe**L05|7(UnvW&^<);*hYGJkm@l= zdKs8#I}5R{TzdEw=nCM2%1AESyAE2XzWodGb>x4U@*C~D^KAD)1&c_1qI@cR+L^h@ zi795?+5)tD(4^7m);M$7y17|d6stC+^NR7iq_y7d{v(jI`z-P zpx*6@0h2;e4m0BYrOAud?J5zctA3NB%v` z56vxb7rF4azKH1VLHU^+bPprH04M$1m~Oxp2aPmM((jWfccc7SDj&}-hb?x=mMz*3 z`ct464M#f{qf>ogHF<)jok0G@&=2u_pYi9iJ8a*@=&Ktu`s#~d%GC{H@@Wv@NAgX` z>BU@^sYmK}`UdDuYaa6IG6rr#Q8RKfhjNo_h4c^e;ipP4_s;t;`ikV2f_)BmJR$NeJSvNO86gaR{^N0 z$zHW6+p907fs;dh3)6P>W60@;9AVds<@ltr_bcT0BLCam-Z6F!&%*PH%ifZICG``s zEYnI|k!>Lg##*+ss5w_QHCE1nKEzbJ@c8l^>fc=Bwb6d2D*ylGTlBrS$ktzIPe2jp zs~U=I8*tf(OAlZJ2n2gEuN(pYEr`(`V>`^8R~qdns%&S*n1S^Fpwke;brCC!BM-*W z;-Z*v@nZhN#cEzLD`cD_+&$Q(<6=zZY(*^b|rwf+Zx1Yuv_i*L3 z6aG^Z=(>S&UH0MA1&A(A7krwEZ0o6HA(b>ziIYk?1yOY#71dMGZ4wFieRQ>$^4jT2 zvag3hDO5ST%6Q-&#LD!|ph40{+TUUR>VYz#0r)#HUyx#OO#@K)f6!GwoJ_t=bh+*6 zV2SO3#hk?O6>-fwpdSVO?=TN!^lR=o>qMUIj-1+DEIO9z<3emdJe*AaBHQ1h9ULbu zvahq%TdI>WZa0D6jb|=}Ny82|zyiMszK_aRV4KioqaM7uA+8s-E0p?_L_v@mD$>WB@E z*B|5J1lCBWas57V;uhHxsd%}80#2IuQaLac>es205x--tH9-7YQ}Gor2PW8>3lGED zA4T-`WAYkJNY!zQ(5cJTfPOdV#XNz29k8{EnVQNe?;ez&K)HxNv-CyLo3#JNqM&pP ztUIE65_Hx7k&AA9iq4gT?k&*m1l_+<1+wI=OVKqHwWs9KSS3ANi?wILH>z={P_H9L=En3+Vd4l}v8P_5->6`wIK^ zy!<7(hnGv)Uo-4@=Wg^1&vRq^bYp6~F2ZUf|H0gDTGa1h)bCO7JO30u3ppOYEWdx1 z?R?JRQD0=cVBd1pr|sLX@&PWi_g`guk~6%3@X=T0-vehZP&}MtTa1fl(A){w2o%(Z ze$;QXX>Vaaucw;R{nLzm4SNfj{Ym4%hx}6HFTpjXbX#}Ew z9Oc6(_o{OEQnga0@*^mxJq^Dqr&Wt|_hKCW3gy+<(^$#nz3F06J^=v}?R~6M^oVmZ z>90jO?TKvQax+~l=v^o;_~H2FK9sLQd6el9%VGsY38(}AHW7c6zsPo&dwolx*=4Xm zipvm|rks^rSdbom3_cF*)uh)=7@TtXChGYX%KK6NO(7VoNzyY{WIpmHV{fPA*<_N| z+vJaP#Kl-6!yl1+#3(>XEnTOBq#5-6hq)iJ{ELWgkdvMO$QRrL{DZ&`;+pKD)n2;7 zt~cAum)fhaCW8Z%`gt691xFC)6CQ-}xJr*xH;pSd?Tak4J0iKJKw-CUfln3q&@}zg z?;{O-q=An#@R0^S(!fU=_(%ieX+U4K@y7zR7%PAhDpDKU%Z^*EZsV|X*e28Jsc`Wf~x9AvnI;S&t^ zFg(ieB*U`|FEE@qjpZ}Ej^WJ=8yK!+=x5l&aFF2+hEFiu!|*7>lMK%?yufhc4J@DG zbqsH2*uZclLqEeFhJy@uFnogH9)?F5o@98I;RS{hZ)EuluVZ*K!v=;c8TuLaFdSsK zgW(el_b@!l@Fc^t3@z4(Wawwu!*Gz{4u(%K+{5rF!;=ipGQ7ZW zVg<`*cpbx=88$Fn$vsN``)hJq!mK?qH~W$lsS zs3}HJZr>sETh>Upd!2-$5P!C>%}%TQ^KqHq&*f*@bK~iz%ul{9p{~i_gi|u@=JX^^ zD?eSrX)e|-<8&2ge3H{x>Wkl}I9<)@YdEcQdOD}eIeinS3picPX$PlkIo;1`7pJ#y zdNHTfJk!eQVJ^R&(>po6j?=q1-O1_QoDOk%FQ*m2{FJ2Y%AYt?mViD^tNHTStFm0B zpWw9W?C*10F*$F+@3rv1<~7Mr;SXBMAK|p(ztd8m^Op3q7qj^#E#;S5%D-euFX6Om z-z}EQcaAPAh)@hto>`k8@hZO^SFt{uL)Ae-$tO z;m0zq=teoM;yJ}%jn8jb!yj^b5~q)Hx{%X9;dBwFU*q&-PQSza zReL#G8?SxJzbU-a%ung>O-^6L2yby(wf6$2mAt%!LtrX>UCe3Kp2?h6`u{kmmAl9iEvt z8U8A-AMfj!Ijhnq-sS?K$=)05w8~f)o)5u8Ia(#2OEsbqtrEx6jY{XjrV7*3$*Imz zys{&Xmxj=jFO`()^2EBdN?%_H$Co8Eqq2nGWs{X?M;Tru;UNOPiUds(8Ngp$R^rVM zS|vTqtX1McvH+06;=!j%qYICC;ep%sE+3evlq7AJ&szx!b-msTG>GwZ2fV05*d+GI zp{J-SJ8;sNs*iKic%@A+ko{na>WIG+lAV$*Df_6lIgrBr3C>XKHj#-x%I;Je$05ZU zO3^R3IYbeiIisI0{-*y}?dB>Jk*z9v)8;@5XOcMMT9HcsvC_{*(FLrn6#cNxA&`eT zqgkZV>q}+FoUP%N4q`2-=o2=FC`xd~@hKun$9zTG6@979)*1k5tSEZ5e%;Lhgj%mM z`WXE!MFGZ6s{U$y7-0eQwK4js@vY=2NN28S?Wxkky#5^K1^P}R#*gYx^;GqzJo>H2 zKhmk9SL@yC%c&G`ngl8Nie9zfL^;tfWIVNgKF;)Ny-oEe8Y}(1$fG7If28&s%CAQz ze#}ND@YmY^^z>3DearO@f!o6L;;gL5k$<>b@LeVO+XRqFzmwzNoik*)sxPCb>aWt< zEcCmXemB!!{qG7>LM2z_{+oqf?XSH0yu_HK5E)wA{{^O3{deYhhrm3;^sisXgbeiT zr1ll0sr`z6?|Fy7*n3_8jn3d_{;cx9gThSu{y#bddjB6K)$B_oRDZ4d+=l`?{;By! z?I)d>LR{1KA`&^$&mpfE{}jF2Z#wlM>0bgZNmBG@c)#S#honCVS~a$Csj2;z1k;zW z{*-;I_A5Ks0-8+uqs1O#qm9z{1o2b&B9W0+`9DWqCcWD4(&*!#_*GjvOwlQru+Td$ zmGT{zO8PEUaluW~bcPE29v8$*>F*#uxkSHRmrMFyivjDRH2sInL}I4%=t|L(f6x3` TaVf22XX3rV@@*CZOH%v)Uh?t* literal 0 HcmV?d00001 diff --git a/usr/sbin/opendmarc-check b/usr/sbin/opendmarc-check new file mode 100755 index 0000000000000000000000000000000000000000..cdd4849e70cf6cd5847bc86e86af36466c03dc50 GIT binary patch literal 10568 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 \$alltables, + 'dbhost=s' => \$dbhost, + 'dbname=s' => \$dbname, + 'dbpasswd=s' => \$dbpasswd, + 'dbport=s' => \$dbport, + 'dbuser=s' => \$dbuser, + 'expire=i' => \$maxage, + 'help!' => \$helponly, + 'verbose!' => \$verbose, + 'version!' => \$showversion, + ); + +if (!$opt_retval || $helponly) +{ + usage(); + + if ($helponly) + { + exit(0); + } + else + { + exit(1); + } +} + +if ($showversion) +{ + print STDOUT "$progname v$version\n"; + exit(0); +} + +# apply defaults +if (!defined($dbhost)) +{ + if (defined($ENV{'OPENDMARC_DBHOST'})) + { + $dbhost = $ENV{'OPENDMARC_DBHOST'}; + } + else + { + $dbhost = $def_dbhost; + } +} + +if (!defined($dbname)) +{ + if (defined($ENV{'OPENDMARC_DB'})) + { + $dbname = $ENV{'OPENDMARC_DB'}; + } + else + { + $dbname = $def_dbname; + } +} + +if (!defined($dbpasswd)) +{ + if (defined($ENV{'OPENDMARC_PASSWORD'})) + { + $dbpasswd = $ENV{'OPENDMARC_PASSWORD'}; + } + else + { + $dbpasswd = $def_dbpasswd; + } +} + +if (!defined($dbport)) +{ + if (defined($ENV{'OPENDMARC_PORT'})) + { + $dbport = $ENV{'OPENDMARC_PORT'}; + } + else + { + $dbport = $def_dbport; + } +} + +if (!defined($dbuser)) +{ + if (defined($ENV{'OPENDMARC_USER'})) + { + $dbuser = $ENV{'OPENDMARC_USER'}; + } + else + { + $dbuser = $def_dbuser; + } +} + +if (!defined($maxage)) +{ + if (defined($ENV{'OPENDMARC_MAXAGE'})) + { + $maxage = $ENV{'OPENDMARC_MAXAGE'}; + } + else + { + $maxage = $def_maxage; + } +} + +# sanity check +if ($maxage <= 0) +{ + print STDERR "$progname: invalid expiration time\n"; + exit(1); +} + +# +# Let's go! +# + +if ($verbose) +{ + print STDERR "$progname: started at " . localtime() . "\n"; +} + +my $dbi_dsn = "DBI:" . $dbscheme . ":database=" . $dbname . + ";host=" . $dbhost . ";port=" . $dbport; + +$dbi_h = DBI->connect($dbi_dsn, $dbuser, $dbpasswd, { PrintError => 0 }); +if (!defined($dbi_h)) +{ + print STDERR "$progname: unable to connect to database: $DBI::errstr\n"; + exit(1); +} + +if ($verbose) +{ + print STDERR "$progname: connected to database\n"; +} + +# +# Expire messages +# + +if ($verbose) +{ + print STDERR "$progname: expiring messages older than $maxage day(s)\n"; +} + +$dbi_s = $dbi_h->prepare("DELETE FROM messages WHERE date <= DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL ? DAY)"); +$rows = $dbi_s->execute($maxage); +if (!$rows) +{ + print STDERR "$progname: DELETE failed: " . $dbi_h->errstr; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); +} +elsif ($verbose) +{ + if ($rows eq "0E0") + { + print STDOUT "$progname: no rows deleted\n"; + } + else + { + print STDOUT "$progname: $rows row(s) deleted\n"; + } +} + +$dbi_s->finish; + +# +# Expire signatures +# + +$dbi_s = $dbi_h->prepare("SELECT MIN(id) FROM messages"); +if (!$dbi_s->execute) +{ + print STDERR "$progname: SELECT failed: " . $dbi_h->errstr; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); +} + +while ($dbi_a = $dbi_s->fetchrow_arrayref()) +{ + $minmsg = $dbi_a->[0]; +} + +# +# We might have emptied the messages table +# +$dbi_s->finish; + +if (!defined($minmsg)) +{ + $dbi_s = $dbi_h->prepare("SELECT COUNT(id) FROM messages"); + if (!$dbi_s->execute) + { + print STDERR "$progname: SELECT failed: " . $dbi_h->errstr; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + + while ($dbi_a = $dbi_s->fetchrow_arrayref()) + { + $rowcount = $dbi_a->[0]; + } + + $dbi_s->finish; + + if (defined($rowcount) && $rowcount == 0) + { + $dbi_s = $dbi_h->prepare("TRUNCATE TABLE signatures"); + if ($dbi_s->execute) + { + print STDERR "$progname: TRUNCATE failed: " . $dbi_h->errstr; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + + $dbi_s->finish; + } + + $dbi_h->disconnect; + exit(1); +} +else +{ + if ($verbose) + { + print STDERR "$progname: expiring signatures on expired messages (id < $minmsg)\n"; + } + + $dbi_s = $dbi_h->prepare("DELETE FROM signatures WHERE message < ?"); + $rows = $dbi_s->execute($minmsg); + if (!$rows) + { + print STDERR "$progname: DELETE failed: " . $dbi_h->errstr; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + elsif ($verbose) + { + if ($rows eq "0E0") + { + print STDOUT "$progname: no rows deleted\n"; + } + else + { + print STDOUT "$progname: $rows row(s) deleted\n"; + } + } + + $dbi_s->finish; +} + +# +# Expire request data +# + +if ($verbose) +{ + print STDERR "$progname: expiring request data older than $maxage days\n"; +} + +$dbi_s = $dbi_h->prepare("DELETE FROM requests WHERE lastsent <= DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL ? DAY) AND NOT lastsent = '0000-00-00 00:00:00'"); +$rows = $dbi_s->execute($maxage); +if (!$rows) +{ + print STDERR "$progname: DELETE failed: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); +} +elsif ($verbose) +{ + if ($rows eq "0E0") + { + print STDOUT "$progname: no rows deleted\n"; + } + else + { + print STDOUT "$progname: $rows row(s) deleted\n"; + } +} + +$dbi_s->finish; + +if ($alltables) +{ + if ($verbose) + { + print STDERR "$progname: expiring unneeded domain data\n"; + } + + $dbi_s = $dbi_h->prepare("DELETE FROM domains WHERE id NOT IN + (SELECT DISTINCT domain FROM requests) AND id NOT IN + (SELECT DISTINCT from_domain FROM messages) AND id NOT IN + (SELECT DISTINCT env_domain FROM messages) AND id NOT IN + (SELECT DISTINCT policy_domain FROM messages) AND id NOT IN + (SELECT DISTINCT domain FROM signatures)"); + $rows = $dbi_s->execute(); + if (!$rows) + { + print STDERR "$progname: DELETE failed: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + elsif ($verbose) + { + if ($rows eq "0E0") + { + print STDOUT "$progname: no rows deleted\n"; + } + else + { + print STDOUT "$progname: $rows row(s) deleted\n"; + } + } + + if ($verbose) + { + print STDERR "$progname: expiring unneeded IP data\n"; + } + + $dbi_s = $dbi_h->prepare("DELETE FROM ipaddr WHERE id NOT IN + (SELECT DISTINCT ip FROM messages)"); + $rows = $dbi_s->execute(); + if (!$rows) + { + print STDERR "$progname: DELETE failed: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + elsif ($verbose) + { + if ($rows eq "0E0") + { + print STDOUT "$progname: no rows deleted\n"; + } + else + { + print STDOUT "$progname: $rows row(s) deleted\n"; + } + } + + if ($verbose) + { + print STDERR "$progname: expiring unneeded reporter data\n"; + } + + $dbi_s = $dbi_h->prepare("DELETE FROM reporters WHERE id NOT IN + (SELECT DISTINCT reporter FROM messages)"); + $rows = $dbi_s->execute(); + if (!$rows) + { + print STDERR "$progname: DELETE failed: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + elsif ($verbose) + { + if ($rows eq "0E0") + { + print STDOUT "$progname: no rows deleted\n"; + } + else + { + print STDOUT "$progname: $rows row(s) deleted\n"; + } + } +} + +# +# All done! +# + +if ($verbose) +{ + print STDERR "$progname: terminating at " . localtime() . "\n"; +} + +$dbi_h->disconnect; + +exit(0); diff --git a/usr/sbin/opendmarc-import b/usr/sbin/opendmarc-import new file mode 100755 index 0000000..d1a241e --- /dev/null +++ b/usr/sbin/opendmarc-import @@ -0,0 +1,606 @@ +#!/usr/bin/perl +# +# Copyright (c) 2012, 2014, The Trusted Domain Project. All rights reserved. +# +# Script to import per-message DMARC data. + +### +### Setup +### + +use strict; +use warnings; + +use Switch; + +use DBI; +use File::Basename; +use Fcntl qw(:flock); +use Getopt::Long; +use POSIX; + +require DBD::mysql; + +# general +my $progname = basename($0); +my $version = "1.3.1"; +my $verbose = 0; +my $helponly = 0; +my $showversion = 0; + +# DB parameters +my $def_dbhost = "localhost"; +my $def_dbname = "opendmarc"; +my $def_dbuser = "opendmarc"; +my $def_dbpasswd = "opendmarc"; +my $def_dbport = "3306"; +my $def_interval = "86400"; +my $dbhost; +my $dbname; +my $dbuser; +my $dbpasswd; +my $dbport; + +my $dbscheme = "mysql"; + +my $dbi_a; +my $dbi_h; +my $dbi_s; +my $dbi_t; + +my $lineno; +my $key; +my $value; + +my $action; +my $adkim; +my $align_dkim; +my $align_spf; +my $aspf; +my $dd; +my $dkim_align; +my @dkim_data; +my $dkim_domain; +my @dkim_entry; +my $dkim_result; +my $envdomain; +my $fdomain; +my $ipaddr; +my $jobid; +my $p; +my $pct; +my $pdomain; +my $policy; +my $received; +my $reporter; +my $repuri; +my $sigcount = 0; +my $sp; +my $spf; +my @rua; + +### +### NO user-serviceable parts beyond this point +### + +sub get_value +{ + my $table; + my $column; + my $id; + my $out; + + ($table, $column, $id) = @_; + + $dbi_t = $dbi_h->prepare("SELECT $column FROM $table WHERE id = ?"); + if (!$dbi_t->execute($id)) + { + print STDERR "$progname: failed to $column value for ID $id: " . $dbi_h->errstr . "\n"; + return undef; + } + + while ($dbi_a = $dbi_t->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $out = $dbi_a->[0]; + } + } + + return $out; +} + +sub get_table_id +{ + my $name; + my $table; + my $column; + my $out; + + ($name, $table, $column) = @_; + + if (!defined($name) || !defined($table)) + { + return undef; + } + + if (!defined($column)) + { + $column = "name"; + } + + $dbi_t = $dbi_h->prepare("SELECT id FROM $table WHERE $column = ?"); + if (!$dbi_t->execute($name)) + { + print STDERR "$progname: failed to retrieve table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + + undef $out; + while ($dbi_a = $dbi_t->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $out = $dbi_a->[0]; + } + } + + $dbi_t->finish; + + if (!defined($out)) + { + $dbi_t = $dbi_h->prepare("INSERT INTO $table ($column) VALUES(?)"); + if (!$dbi_t->execute($name)) + { + print STDERR "$progname: failed to create table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + + $dbi_t = $dbi_h->prepare("SELECT LAST_INSERT_ID()"); + if (!$dbi_t->execute()) + { + print STDERR "$progname: failed to retrieve created table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + + while ($dbi_a = $dbi_t->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $out = $dbi_a->[0]; + } + } + + $dbi_t->finish; + + if (!defined($out)) + { + print STDERR "$progname: failed to retrieve created table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + } + + return $out; +} + +sub update_db +{ + my $rep_id; + my $from_id; + my $envfrom_id; + my $pdomain_id; + my $ipaddr_id; + my $msg_id; + my $sdomain_id; + my $request_id; + + if ($verbose) + { + print STDERR "$progname: updating at line $lineno\n"; + } + + $rep_id = get_table_id($reporter, "reporters"); + $from_id = get_table_id($fdomain, "domains"); + $envfrom_id = get_table_id($envdomain, "domains"); + $pdomain_id = get_table_id($pdomain, "domains"); + $ipaddr_id = get_table_id($ipaddr, "ipaddr", "addr"); + $request_id = get_table_id($from_id, "requests", "domain"); + + if (!defined($rep_id) || + !defined($from_id) || + !defined($envfrom_id) || + !defined($pdomain_id) || + !defined($ipaddr_id) || + !defined($request_id)) + { + return; + } + + $dbi_s = $dbi_h->prepare("INSERT INTO messages (date, jobid, reporter, policy, disp, ip, env_domain, from_domain, spf, align_spf, align_dkim, sigcount) VALUES(FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + if (!$dbi_s->execute($received, $jobid, $rep_id, $policy, $action, $ipaddr_id, $envfrom_id, $from_id, $spf, $align_spf, $align_dkim, $sigcount)) + { + print STDERR "$progname: failed to insert message: " . $dbi_h->errstr . "\n"; + return; + } + + $dbi_s->finish; + + undef $msg_id; + $dbi_s = $dbi_h->prepare("SELECT LAST_INSERT_ID()"); + if (!$dbi_s->execute()) + { + print STDERR "$progname: failed to retrieve message ID: " . $dbi_h->errstr . "\n"; + return; + } + + while ($dbi_a = $dbi_s->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $msg_id = $dbi_a->[0]; + } + } + + $dbi_s->finish; + + if (!defined($msg_id)) + { + print STDERR "$progname: failed to retrieve message ID: " . $dbi_h->errstr . "\n"; + return; + } + + $dbi_s = $dbi_h->prepare("INSERT INTO signatures (message, domain, pass, error) VALUES(?, ?, ?, ?)"); + foreach $dd (0 .. $#dkim_data) + { + my $sdomain; + my $pass; + my $error; + + $sdomain = $dkim_data[$dd][0]; + $pass = $dkim_data[$dd][1]; + $error = $dkim_data[$dd][2]; + + $sdomain_id = get_table_id($sdomain, "domains"); + if (!defined($sdomain_id)) + { + next; + } + + if (!$dbi_s->execute($msg_id, $sdomain_id, $pass, $error)) + { + print STDERR "$progname: failed to insert DKIM data: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + return; + } + } + $dbi_s->finish; + + if (get_value("requests", "locked", $request_id) != 1) + { + if (scalar @rua > 0) + { + $repuri = join(",", @rua); + $dbi_s = $dbi_h->prepare("UPDATE requests SET repuri = ? WHERE id = ?"); + + if (!$dbi_s->execute($repuri, $request_id)) + { + print STDERR "$progname: failed to update reporting URI for $fdomain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + return; + } + + $dbi_s->finish; + } + else + { + $dbi_s = $dbi_h->prepare("UPDATE requests SET repuri = NULL WHERE id = ?"); + + if (!$dbi_s->execute($request_id)) + { + print STDERR "$progname: failed to update reporting URI for $fdomain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + return; + } + + $dbi_s->finish; + } + + $dbi_s = $dbi_h->prepare("UPDATE requests SET adkim = ?, aspf = ?, policy = ?, spolicy = ?, pct = ? WHERE id = ?"); + + if (!$dbi_s->execute($adkim, $aspf, $p, $sp, $pct, $request_id)) + { + print STDERR "$progname: failed to update policy data for $fdomain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + return; + } + } + + $dbi_s->finish; +} + +sub usage +{ + print STDERR "$progname: usage: $progname [options]\n"; + print STDERR "\t--dbhost=host database host [$def_dbhost]\n"; + print STDERR "\t--dbname=name database name [$def_dbname]\n"; + print STDERR "\t--dbpasswd=passwd database password [$def_dbpasswd]\n"; + print STDERR "\t--dbport=port database port [$def_dbport]\n"; + print STDERR "\t--dbuser=user database user [$def_dbuser]\n"; + print STDERR "\t--help print help and exit\n"; + print STDERR "\t--verbose verbose output\n"; + print STDERR "\t--version print version and exit\n"; +} + +# parse command line arguments +my $opt_retval = &Getopt::Long::GetOptions ('dbhost=s' => \$dbhost, + 'dbname=s' => \$dbname, + 'dbpasswd=s' => \$dbpasswd, + 'dbport=s' => \$dbport, + 'dbuser=s' => \$dbuser, + 'help!' => \$helponly, + 'verbose!' => \$verbose, + 'version!' => \$showversion, + ); + +if (!$opt_retval || $helponly) +{ + usage(); + + if ($helponly) + { + exit(0); + } + else + { + exit(1); + } +} + +if ($showversion) +{ + print STDOUT "$progname v$version\n"; + exit(0); +} + +# apply defaults +if (!defined($dbhost)) +{ + if (defined($ENV{'OPENDMARC_DBHOST'})) + { + $dbhost = $ENV{'OPENDMARC_DBHOST'}; + } + else + { + $dbhost = $def_dbhost; + } +} + +if (!defined($dbname)) +{ + if (defined($ENV{'OPENDMARC_DB'})) + { + $dbname = $ENV{'OPENDMARC_DB'}; + } + else + { + $dbname = $def_dbname; + } +} + +if (!defined($dbpasswd)) +{ + if (defined($ENV{'OPENDMARC_PASSWORD'})) + { + $dbpasswd = $ENV{'OPENDMARC_PASSWORD'}; + } + else + { + $dbpasswd = $def_dbpasswd; + } +} + +if (!defined($dbport)) +{ + if (defined($ENV{'OPENDMARC_PORT'})) + { + $dbport = $ENV{'OPENDMARC_PORT'}; + } + else + { + $dbport = $def_dbport; + } +} + +if (!defined($dbuser)) +{ + if (defined($ENV{'OPENDMARC_USER'})) + { + $dbuser = $ENV{'OPENDMARC_USER'}; + } + else + { + $dbuser = $def_dbuser; + } +} + +if ($verbose) +{ + print STDERR "$progname: started at " . localtime() . "\n"; +} + +my $dbi_dsn = "DBI:" . $dbscheme . ":database=" . $dbname . + ";host=" . $dbhost . ";port=" . $dbport; + +$dbi_h = DBI->connect($dbi_dsn, $dbuser, $dbpasswd, { PrintError => 0 }); +if (!defined($dbi_h)) +{ + print STDERR "$progname: unable to connect to database: $DBI::errstr\n"; + exit(1); +} + +if ($verbose) +{ + print STDERR "$progname: connected to database\n"; +} + +# +# Read history file from stdin. +# + +$lineno = 0; +if (!flock(STDIN, LOCK_SH)) +{ + print STDERR "$progname: warning: unable to establish read lock\n"; +} + +while () +{ + $lineno++; + + chomp; + ($key, $value, $dkim_result) = split; + + switch ($key) + { + case "action" { + $action = $value; + } + + case "adkim" { + $adkim = $value; + } + + case "align_dkim" { + $align_dkim = $value; + } + + case "align_spf" { + $align_spf = $value; + } + + case "aspf" { + $aspf = $value; + } + + case "dkim" { + undef @dkim_entry; + push(@dkim_entry, $value); + push(@dkim_entry, $dkim_result); + if ($dkim_result eq "4" || + $dkim_result eq "5") + { + push(@dkim_entry, 1); + } + else + { + push(@dkim_entry, 0); + } + push(@dkim_data, [ @dkim_entry ]); + + $sigcount++; + } + + case "from" { + $fdomain = $value; + } + + case "job" { + if (defined($jobid)) + { + update_db(); + + undef $action; + undef $adkim; + undef $align_dkim; + undef $align_spf; + undef $aspf; + undef @dkim_data; + undef $envdomain; + undef $fdomain; + undef $ipaddr; + undef $jobid; + undef $p; + undef $pct; + undef $pdomain; + undef $policy; + undef $received; + undef $reporter; + undef @rua; + $sigcount = 0; + undef $sp; + undef $spf; + } + + $jobid = $value; + } + + case "ipaddr" { + $ipaddr = $value; + } + + case "mfrom" { + $envdomain = $value; + } + + case "p" { + $p = $value; + } + + case "pct" { + $pct = $value; + } + + case "pdomain" { + $pdomain = $value; + } + + case "policy" { + $policy = $value; + } + + case "received" { + $received = $value; + } + + case "reporter" { + $reporter = $value; + } + + case "rua" { + if ($value ne "-") + { + push(@rua, $value); + } + } + + case "sp" { + $sp = $value; + } + + case "spf" { + $spf = $value; + } + + else { + print STDERR "$progname: unknown key '$key' at line $lineno\n"; + } + } +} + +if (defined($jobid)) +{ + update_db(); +} + +# +# all done! +# + +if ($verbose) +{ + print STDERR "$progname: terminating at " . localtime() . "\n"; +} + +$dbi_h->disconnect; + +exit(0); diff --git a/usr/sbin/opendmarc-importstats b/usr/sbin/opendmarc-importstats new file mode 100755 index 0000000..839a871 --- /dev/null +++ b/usr/sbin/opendmarc-importstats @@ -0,0 +1,26 @@ +#!/bin/sh +## +## Copyright (c) 2012, The Trusted Domain Project. All rights reserved. +## +## opendmarc-importstats -- import opendmarc output to MySQL +## +## This is intended to be used via a crontab. If import is successful, +## this code exits quietly so there's no output. If it fails, it does +## "ls -l" on the temporary file, so that cron generates mail to whever +## ran the job. + +## setup +statsdb="/var/tmp/dmarc.dat" +# OPENDMARC_PASSWORD="password"; export OPENDMARC_PASSWORD + +if [ -s $statsdb ] +then + mv $statsdb ${statsdb}.OLD.$$ + + if opendmarc-import < ${statsdb}.OLD.$$ + then + rm ${statsdb}.OLD.$$ + else + ls -l ${statsdb}.OLD.$$ + fi +fi diff --git a/usr/sbin/opendmarc-params b/usr/sbin/opendmarc-params new file mode 100755 index 0000000..726cd1f --- /dev/null +++ b/usr/sbin/opendmarc-params @@ -0,0 +1,284 @@ +#!/usr/bin/perl +# +# Copyright (c) 2012, 2013, The Trusted Domain Project. All rights reserved. +# +# Script to apply manual changes to DMARC reporting parameters. + +### +### Setup +### + +use strict; +use warnings; + +use Switch; + +use DBI; +use File::Basename; +use Getopt::Long; +use POSIX; + +require DBD::mysql; + +# general +my $progname = basename($0); +my $version = "1.3.1"; +my $verbose = 0; +my $helponly = 0; +my $showversion = 0; + +# DB parameters +my $def_dbhost = "localhost"; +my $def_dbname = "opendmarc"; +my $def_dbuser = "opendmarc"; +my $def_dbpasswd = "opendmarc"; +my $def_dbport = "3306"; +my $dbhost; +my $dbname; +my $dbuser; +my $dbpasswd; +my $dbport; + +my $dbscheme = "mysql"; + +my $dbi_a; +my $dbi_h; +my $dbi_t; + +my $domain; +my $domainid; +my $requestid; +my $rua; +my $unlock; + +sub get_table_id +{ + my $name; + my $table; + my $column; + my $out; + + ($name, $table, $column) = @_; + + if (!defined($name) || !defined($table)) + { + return undef; + } + + if (!defined($column)) + { + $column = "name"; + } + + $dbi_t = $dbi_h->prepare("SELECT id FROM $table WHERE $column = ?"); + if (!$dbi_t->execute($name)) + { + print STDERR "$progname: failed to retrieve table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + + undef $out; + while ($dbi_a = $dbi_t->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $out = $dbi_a->[0]; + } + } + + $dbi_t->finish; + + if (!defined($out)) + { + $dbi_t = $dbi_h->prepare("INSERT INTO $table ($column) VALUES(?)"); + if (!$dbi_t->execute($name)) + { + print STDERR "$progname: failed to create table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + + $dbi_t = $dbi_h->prepare("SELECT LAST_INSERT_ID()"); + if (!$dbi_t->execute()) + { + print STDERR "$progname: failed to retrieve created table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + + while ($dbi_a = $dbi_t->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $out = $dbi_a->[0]; + } + } + + $dbi_t->finish; + + if (!defined($out)) + { + print STDERR "$progname: failed to retrieve created table ID: " . $dbi_h->errstr . "\n"; + return undef; + } + } + + return $out; +} + +sub usage +{ + print STDERR "$progname: usage: $progname [options] domain\n"; + print STDERR "\t--dbhost=host database host [$def_dbhost]\n"; + print STDERR "\t--dbname=name database name [$def_dbname]\n"; + print STDERR "\t--dbpasswd=passwd database password [$def_dbpasswd]\n"; + print STDERR "\t--dbport=port database port [$def_dbport]\n"; + print STDERR "\t--dbuser=user database user [$def_dbuser]\n"; + print STDERR "\t--rua=string aggregate report URI(s)\n"; + print STDERR "\t--help print help and exit\n"; + print STDERR "\t--unlock unlocks named record\n"; + print STDERR "\t--verbose verbose output\n"; + print STDERR "\t--version print version and exit\n"; +} + +# parse command line arguments +my $opt_retval = &Getopt::Long::GetOptions ('dbhost=s' => \$dbhost, + 'dbname=s' => \$dbname, + 'dbpasswd=s' => \$dbpasswd, + 'dbport=s' => \$dbport, + 'dbuser=s' => \$dbuser, + 'help!' => \$helponly, + 'rua=s' => \$rua, + 'unlock!' => \$unlock, + 'verbose!' => \$verbose, + 'version!' => \$showversion, + ); + +$domain = $ARGV[0]; + +if (!$opt_retval || $helponly || !defined($domain)) +{ + usage(); + + if ($helponly) + { + exit(0); + } + else + { + exit(1); + } +} + +if ($showversion) +{ + print STDOUT "$progname v$version\n"; + exit(0); +} + +# apply defaults +if (!defined($dbhost)) +{ + if (defined($ENV{'OPENDMARC_DBHOST'})) + { + $dbhost = $ENV{'OPENDMARC_DBHOST'}; + } + else + { + $dbhost = $def_dbhost; + } +} + +if (!defined($dbname)) +{ + if (defined($ENV{'OPENDMARC_DB'})) + { + $dbname = $ENV{'OPENDMARC_DB'}; + } + else + { + $dbname = $def_dbname; + } +} + +if (!defined($dbpasswd)) +{ + if (defined($ENV{'OPENDMARC_PASSWORD'})) + { + $dbpasswd = $ENV{'OPENDMARC_PASSWORD'}; + } + else + { + $dbpasswd = $def_dbpasswd; + } +} + +if (!defined($dbport)) +{ + if (defined($ENV{'OPENDMARC_PORT'})) + { + $dbport = $ENV{'OPENDMARC_PORT'}; + } + else + { + $dbport = $def_dbport; + } +} + +if (!defined($dbuser)) +{ + if (defined($ENV{'OPENDMARC_USER'})) + { + $dbuser = $ENV{'OPENDMARC_USER'}; + } + else + { + $dbuser = $def_dbuser; + } +} + +if ($verbose) +{ + print STDERR "$progname: started at " . localtime() . "\n"; +} + +my $dbi_dsn = "DBI:" . $dbscheme . ":database=" . $dbname . + ";host=" . $dbhost . ";port=" . $dbport; + +$dbi_h = DBI->connect($dbi_dsn, $dbuser, $dbpasswd, { PrintError => 0 }); +if (!defined($dbi_h)) +{ + print STDERR "$progname: unable to connect to database: $DBI::errstr\n"; + exit(1); +} + +if ($verbose) +{ + print STDERR "$progname: connected to database\n"; +} + +$domainid = get_table_id($domain, "domains", "name"); +$requestid = get_table_id($domainid, "requests", "domain"); + +if ($unlock) +{ + $dbi_t = $dbi_h->prepare("UPDATE requests SET locked = 0 WHERE id = ?"); + if (!$dbi_t->execute($requestid)) + { + print STDERR "$progname: failed to update requests table for $domain: " . $dbi_h->errstr . "\n"; + } +} +else +{ + $dbi_t = $dbi_h->prepare("UPDATE requests SET locked = 1, repuri = ? WHERE id = ?"); + if (!$dbi_t->execute($rua, $requestid)) + { + print STDERR "$progname: failed to update requests table for $domain: " . $dbi_h->errstr . "\n"; + } +} + +# +# all done! +# + +$dbi_h->disconnect; + +exit(0); diff --git a/usr/sbin/opendmarc-reports b/usr/sbin/opendmarc-reports new file mode 100755 index 0000000..7616d8d --- /dev/null +++ b/usr/sbin/opendmarc-reports @@ -0,0 +1,991 @@ +#!/usr/bin/perl +# +# Copyright (c) 2012-2015, The Trusted Domain Project. All rights reserved. +# +# Script to generate regular DMARC reports. + +### +### Setup +### + +use strict; +use warnings; + +use Switch; + +use DBI; +use File::Basename; +use File::Temp; +use Net::Domain qw(hostfqdn hostdomain); +use Getopt::Long; +use IO::Handle; +use IO::Compress::Zip qw(zip); +use POSIX; +use MIME::Base64; +use Net::SMTP; + +require DBD::mysql; + +require HTTP::Request; + +# general +my $progname = basename($0); +my $version = "1.3.1"; +my $verbose = 0; +my $helponly = 0; +my $showversion = 0; + +my $interval; + +my $gen; +my $uri; + +my $buf; + +my $mailout; +my $boundary; + +my $tmpout; + +my $repfile; +my $zipfile; + +my $zipin; + +my $now; + +my $repstart; +my $repend; + +my $domain; +my $domainid; +my $domainset; +my $forcedomain; +my @skipdomains; + +my $policy; +my $spolicy; +my $policystr; +my $spolicystr; +my $pct; + +my $repuri; +my @repuris; +my $lastsent; + +my $aspf; +my $aspfstr; +my $adkim; +my $adkimstr; +my $align_dkim; +my $align_dkimstr; +my $align_spf; +my $align_spfstr; +my $spfresult; +my $dkimresult; +my $disp; +my $spfresultstr; +my $dkimresultstr; +my $dispstr; +my $ipaddr; +my $fromdomain; +my $envdomain; +my $dkimdomain; + +my $repdest; + +my $smtpstatus; +my $smtpfail; + +my $doupdate = 1; +my $testmode = 0; +my $keepfiles = 0; +my $use_utc = 0; +my $daybound = 0; +my $report_maxbytes_global = 15728640; # default: 15M, per spec + +my $msgid; + +my $rowcount; + +my $dbi_s; +my $dbi_h; +my $dbi_a; +my $dbi_d; + +# DB parameters +my $def_dbhost = "localhost"; +my $def_dbname = "opendmarc"; +my $def_dbuser = "opendmarc"; +my $def_dbpasswd = "opendmarc"; +my $def_dbport = "3306"; +my $def_interval = "86400"; +my $dbhost; +my $dbname; +my $dbuser; +my $dbpasswd; +my $dbport; + +my $dbscheme = "mysql"; + +my $repdom = hostdomain(); +my $repemail = "postmaster@" . $repdom; + +my $smtp_server = '127.0.0.1'; +my $smtp_port = 25; +my $smtp; + +my $answer; + +### +### NO user-serviceable parts beyond this point +### + +sub usage +{ + print STDERR "$progname: usage: $progname [options]\n"; + print STDERR "\t--day send yesterday's data\n"; + print STDERR "\t--dbhost=host database host [$def_dbhost]\n"; + print STDERR "\t--dbname=name database name [$def_dbname]\n"; + print STDERR "\t--dbpasswd=passwd database password [$def_dbpasswd]\n"; + print STDERR "\t--dbport=port database port [$def_dbport]\n"; + print STDERR "\t--dbuser=user database user [$def_dbuser]\n"; + print STDERR "\t--domain=name force a report for named domain\n"; + print STDERR "\t--help print help and exit\n"; + print STDERR "\t--interval=secs report interval [$def_interval]\n"; + print STDERR "\t--keepfiles keep xml files (in local directory)\n"; + print STDERR "\t -n synonym for --test\n"; + print STDERR "\t--nodomain=name omit a report for named domain\n"; + print STDERR "\t--noupdate don't record report transmission\n"; + print STDERR "\t--report-email reporting contact [$repemail]\n"; + print STDERR "\t--report-org reporting organization [$repdom]\n"; + print STDERR "\t--smtp-port smtp server port [$smtp_port]\n"; + print STDERR "\t--smtp-server smtp server [$smtp_server]\n"; + print STDERR "\t--test don't send reports\n"; + print STDERR "\t--utc operate in UTC\n"; + print STDERR "\t (implies --keepfiles --noupdate)\n"; + print STDERR "\t--verbose verbose output\n"; + print STDERR "\t (repeat for increased output)\n"; + print STDERR "\t--version print version and exit\n"; +} + +# set locale +setlocale(LC_ALL, 'C'); + +# parse command line arguments +my $opt_retval = &Getopt::Long::GetOptions ('day!' => \$daybound, + 'dbhost=s' => \$dbhost, + 'dbname=s' => \$dbname, + 'dbpasswd=s' => \$dbpasswd, + 'dbport=s' => \$dbport, + 'dbuser=s' => \$dbuser, + 'domain=s' => \$forcedomain, + 'help!' => \$helponly, + 'interval=i' => \$interval, + 'keepfiles' => \$keepfiles, + 'n|test' => \$testmode, + 'nodomain=s' => \@skipdomains, + 'report-email=s' => \$repemail, + 'report-org=s' => \$repdom, + 'smtp-server=s' => \$smtp_server, + 'smtp-port=i' => \$smtp_port, + 'update!' => \$doupdate, + 'utc!' => \$use_utc, + 'verbose+' => \$verbose, + 'version!' => \$showversion, + ); + +if (!$opt_retval || $helponly) +{ + usage(); + + if ($helponly) + { + exit(0); + } + else + { + exit(1); + } +} + +if ($showversion) +{ + print STDOUT "$progname v$version\n"; + exit(0); +} + +# apply defaults +if (!defined($dbhost)) +{ + if (defined($ENV{'OPENDMARC_DBHOST'})) + { + $dbhost = $ENV{'OPENDMARC_DBHOST'}; + } + else + { + $dbhost = $def_dbhost; + } +} + +if (!defined($dbname)) +{ + if (defined($ENV{'OPENDMARC_DB'})) + { + $dbname = $ENV{'OPENDMARC_DB'}; + } + else + { + $dbname = $def_dbname; + } +} + +if (!defined($dbpasswd)) +{ + if (defined($ENV{'OPENDMARC_PASSWORD'})) + { + $dbpasswd = $ENV{'OPENDMARC_PASSWORD'}; + } + else + { + $dbpasswd = $def_dbpasswd; + } +} + +if (!defined($dbport)) +{ + if (defined($ENV{'OPENDMARC_PORT'})) + { + $dbport = $ENV{'OPENDMARC_PORT'}; + } + else + { + $dbport = $def_dbport; + } +} + +if (!defined($dbuser)) +{ + if (defined($ENV{'OPENDMARC_USER'})) + { + $dbuser = $ENV{'OPENDMARC_USER'}; + } + else + { + $dbuser = $def_dbuser; + } +} + +if (!defined($interval)) +{ + $interval = $def_interval; +} + +# Test mode requested, don't update last sent and keep xml files +$doupdate = ($testmode == 1) ? 0 : $doupdate; +$keepfiles = ($testmode == 1) ? 1 : $keepfiles; + +if ($verbose) +{ + print STDERR "$progname: started at " . localtime() . "\n"; +} + +my $dbi_dsn = "DBI:" . $dbscheme . ":database=" . $dbname . + ";host=" . $dbhost . ";port=" . $dbport; + +$dbi_h = DBI->connect($dbi_dsn, $dbuser, $dbpasswd, { PrintError => 0 }); +if (!defined($dbi_h)) +{ + print STDERR "$progname: unable to connect to database: $DBI::errstr\n"; + exit(1); +} + +if ($verbose >= 2) +{ + print STDERR "$progname: connected to database\n"; +} + +if ($use_utc) +{ + $dbi_s = $dbi_h->prepare("SET TIME_ZONE='+00:00'"); + + if (!$dbi_s->execute()) + { + print STDERR "$progname: failed to change to UTC: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } +} + +# +# Select domains on which to report +# + +$now = time(); + +if ($verbose >= 2) +{ + print STDERR "$progname: selecting target domains\n"; +} + +if (defined($forcedomain)) +{ + $dbi_s = $dbi_h->prepare("SELECT name FROM domains WHERE name = ?"); + + if (!$dbi_s->execute($forcedomain)) + { + print STDERR "$progname: failed to test for database entry: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } +} +elsif ($daybound) +{ + $dbi_s = $dbi_h->prepare("SELECT domains.name FROM requests JOIN domains ON requests.domain = domains.id WHERE DATE(lastsent) < DATE(FROM_UNIXTIME(?))"); + + if (!$dbi_s->execute($now)) + { + print STDERR "$progname: failed to collect domain names: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } +} +else +{ + $dbi_s = $dbi_h->prepare("SELECT domains.name FROM requests JOIN domains ON requests.domain = domains.id WHERE lastsent <= DATE_SUB(FROM_UNIXTIME(?), INTERVAL ? SECOND)"); + + if (!$dbi_s->execute($now, $interval)) + { + print STDERR "$progname: failed to collect domain names: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } +} + +$domainset = $dbi_s->fetchall_arrayref([0]); +$dbi_s->finish; + +if ($verbose) +{ + print STDERR "$progname: selected " . scalar(@$domainset) . " domain(s)\n"; +} + +# +# For each domain: +# -- extract reporting address +# -- extract messages/signatures to report +# -- generate and send report +# -- update "last sent" timestamp +# + +$smtp = Net::SMTP->new($smtp_server, + 'Port' => $smtp_port, + 'Helo' => hostfqdn()); +if (!defined($smtp)) +{ + print STDERR "$progname: open SMTP server $smtp_server:$smtp_port failed\n"; + exit(1); +} + +foreach (@$domainset) +{ + $domain = $_->[0]; + + if (!defined($domain)) + { + next; + } + + if (@skipdomains && grep({$_ eq $domain} @skipdomains) != 0) + { + next; + } + + if ($verbose >= 2) + { + print STDERR "$progname: processing $domain\n"; + } + + # extract this domain's reporting parameters + $dbi_s = $dbi_h->prepare("SELECT id FROM domains WHERE name = ?"); + if (!$dbi_s->execute($domain)) + { + print STDERR "$progname: can't get ID for domain $domain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + + undef $domainid; + while ($dbi_a = $dbi_s->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $domainid = $dbi_a->[0]; + } + } + $dbi_s->finish; + + if (!defined($domainid)) + { + print STDERR "$progname: ID for domain $domain not found\n"; + next; + } + + $dbi_s = $dbi_h->prepare("SELECT repuri, adkim, aspf, policy, spolicy, pct, UNIX_TIMESTAMP(lastsent) FROM requests WHERE domain = ?"); + if (!$dbi_s->execute($domainid)) + { + print STDERR "$progname: can't get reporting URI for domain $domain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + + undef $repuri; + + while ($dbi_a = $dbi_s->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $repuri = $dbi_a->[0]; + } + if (defined($dbi_a->[1])) + { + $adkim = $dbi_a->[1]; + } + if (defined($dbi_a->[2])) + { + $aspf = $dbi_a->[2]; + } + if (defined($dbi_a->[3])) + { + $policy = $dbi_a->[3]; + } + if (defined($dbi_a->[4])) + { + $spolicy = $dbi_a->[4]; + } + if (defined($dbi_a->[5])) + { + $pct = $dbi_a->[5]; + } + if (defined($dbi_a->[6])) + { + $lastsent = $dbi_a->[6]; + } + } + + $dbi_s->finish; + + if (!defined($repuri) || ("" eq $repuri)) + { + if ($verbose >= 2) + { + print STDERR "$progname: no reporting URI for domain $domain; skipping\n"; + } + + next; + } + + # construct the temporary file + $repfile = $repdom . "!" . $domain . "!" . $lastsent . "!" . time() . ".xml"; + $zipfile = $repdom . "!" . $domain . "!" . $lastsent . "!" . time() . ".zip"; + if (!open($tmpout, ">", $repfile)) + { + print STDERR "$progname: can't create report file for domain $domain\n"; + next; + } + + switch ($adkim) + { + case ord("r") { $adkimstr = "r"; } + case ord("s") { $adkimstr = "s"; } + else { $adkimstr = "unknown"; } + } + + switch ($aspf) + { + case ord("r") { $aspfstr = "r"; } + case ord("s") { $aspfstr = "s"; } + else { $aspfstr = "unknown"; } + } + + switch ($policy) + { + case ord("n") { $policystr = "none"; } + case ord("q") { $policystr = "quarantine"; } + case ord("r") { $policystr = "reject"; } + else { $policystr = "unknown"; } + } + + switch ($spolicy) + { + case ord("n") { $spolicystr = "none"; } + case ord("q") { $spolicystr = "quarantine"; } + case ord("r") { $spolicystr = "reject"; } + } + + if ($daybound) + { + $dbi_s = $dbi_h->prepare("SELECT UNIX_TIMESTAMP(MIN(date)), UNIX_TIMESTAMP(MAX(date)) FROM messages WHERE from_domain = ? AND DATE(date) >= DATE(FROM_UNIXTIME(?)) AND DATE(date) < DATE(FROM_UNIXTIME(?))"); + } + else + { + $dbi_s = $dbi_h->prepare("SELECT UNIX_TIMESTAMP(MIN(date)), UNIX_TIMESTAMP(MAX(date)) FROM messages WHERE from_domain = ? AND UNIX_TIMESTAMP(date) > ? AND UNIX_TIMESTAMP(date) <= ?"); + } + + if (!$dbi_s->execute($domainid, $lastsent, $now)) + { + print STDERR "$progname: can't extract begin/end times for domain $domain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + + $repstart = 0; + $repend = $now; + + while ($dbi_a = $dbi_s->fetchrow_arrayref()) + { + if (defined($dbi_a->[0])) + { + $repstart = $dbi_a->[0]; + } + if (defined($dbi_a->[1])) + { + $repend = $dbi_a->[1]; + } + } + + $dbi_s->finish; + + print $tmpout "\n"; + print $tmpout "\n"; + + print $tmpout " \n"; + print $tmpout " $repdom\n"; + print $tmpout " $repemail\n"; + print $tmpout " $domain:$now\n"; + print $tmpout " \n"; + print $tmpout " $repstart\n"; + print $tmpout " $repend\n"; + print $tmpout " \n"; + print $tmpout " \n"; + + print $tmpout " \n"; + print $tmpout " $domain\n"; + print $tmpout " $adkimstr\n"; + print $tmpout " $aspfstr\n"; + print $tmpout "

$policystr

\n"; + if (defined($spolicystr)) + { + print $tmpout " $spolicystr\n"; + } + print $tmpout " $pct\n"; + print $tmpout "
\n"; + + if ($daybound) + { + $dbi_s = $dbi_h->prepare("SELECT messages.id, ipaddr.addr, messages.disp, d1.name, d2.name, messages.spf, messages.align_spf, messages.align_dkim FROM messages JOIN ipaddr ON messages.ip = ipaddr.id JOIN domains d1 ON messages.from_domain = d1.id JOIN domains d2 ON messages.env_domain = d2.id WHERE messages.from_domain = ? AND DATE(messages.date) >= DATE(FROM_UNIXTIME(?)) AND DATE(messages.date) < DATE(FROM_UNIXTIME(?))"); + } + else + { + $dbi_s = $dbi_h->prepare("SELECT messages.id, ipaddr.addr, messages.disp, d1.name, d2.name, messages.spf, messages.align_spf, messages.align_dkim FROM messages JOIN ipaddr ON messages.ip = ipaddr.id JOIN domains d1 ON messages.from_domain = d1.id JOIN domains d2 ON messages.env_domain = d2.id WHERE messages.from_domain = ? AND messages.date > FROM_UNIXTIME(?) AND messages.date <= FROM_UNIXTIME(?)"); + } + + if (!$dbi_s->execute($domainid, $lastsent, $now)) + { + print STDERR "$progname: can't extract report for domain $domain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + + $rowcount = 0; + + while ($dbi_a = $dbi_s->fetchrow_arrayref()) + { + undef $msgid; + + if (defined($dbi_a->[0])) + { + $msgid = $dbi_a->[0]; + } + if (defined($dbi_a->[1])) + { + $ipaddr = $dbi_a->[1]; + } + if (defined($dbi_a->[2])) + { + $disp = $dbi_a->[2]; + } + if (defined($dbi_a->[3])) + { + $fromdomain = $dbi_a->[3]; + } + if (defined($dbi_a->[4])) + { + $envdomain = $dbi_a->[4]; + } + if (defined($dbi_a->[5])) + { + $spfresult = $dbi_a->[5]; + } + if (defined($dbi_a->[6])) + { + $align_spf = $dbi_a->[6]; + } + if (defined($dbi_a->[7])) + { + $align_dkim = $dbi_a->[7]; + } + + if (!defined($msgid)) + { + next; + } + + $rowcount++; + + switch ($disp) + { + case 0 { $dispstr = "reject"; } + case 1 { $dispstr = "reject"; } + case 2 { $dispstr = "none"; } + case 4 { $dispstr = "quarantine"; } + else { $dispstr = "unknown"; } + } + + switch ($spfresult) + { + case 0 { $spfresultstr = "pass"; } + case 2 { $spfresultstr = "softfail"; } + case 3 { $spfresultstr = "neutral"; } + case 4 { $spfresultstr = "temperror"; } + case 5 { $spfresultstr = "permerror"; } + case 6 { $spfresultstr = "none"; } + case 7 { $spfresultstr = "fail"; } + case 8 { $spfresultstr = "policy"; } + case 9 { $spfresultstr = "nxdomain"; } + case 10 { $spfresultstr = "signed"; } + case 12 { $spfresultstr = "discard"; } + else { $spfresultstr = "unknown"; } + } + + switch ($align_dkim) + { + case 4 { $align_dkimstr = "pass"; } + case 5 { $align_dkimstr = "fail"; } + else { $align_dkimstr = "unknown"; } + } + + switch ($align_spf) + { + case 4 { $align_spfstr = "pass"; } + case 5 { $align_spfstr = "fail"; } + else { $align_spfstr = "unknown"; } + } + + print $tmpout " \n"; + print $tmpout " \n"; + print $tmpout " $ipaddr\n"; + print $tmpout " 1\n"; + print $tmpout " \n"; + print $tmpout " $dispstr\n"; + print $tmpout " $align_dkimstr\n"; + print $tmpout " $align_spfstr\n"; + print $tmpout " \n"; + print $tmpout " \n"; + print $tmpout " \n"; + print $tmpout " $fromdomain\n"; + print $tmpout " \n"; + print $tmpout " \n"; + print $tmpout " \n"; + print $tmpout " $envdomain\n"; + print $tmpout " $spfresultstr\n"; + print $tmpout " \n"; + + $dbi_d = $dbi_h->prepare("SELECT domains.name, pass FROM signatures JOIN domains ON signatures.domain = domains.id WHERE signatures.message = ?"); + if (!$dbi_d->execute($msgid)) + { + print STDERR "$progname: can't extract report for message $msgid: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_d->finish; + $dbi_h->disconnect; + exit(1); + } + + while ($dbi_a = $dbi_d->fetchrow_arrayref()) + { + undef $dkimdomain; + + if (defined($dbi_a->[0])) + { + $dkimdomain = $dbi_a->[0]; + } + if (defined($dbi_a->[1])) + { + $dkimresult = $dbi_a->[1]; + } + + + if (!defined($dkimdomain)) + { + next; + } + + switch ($dkimresult) + { + case 0 { $dkimresultstr = "pass"; } + case 2 { $dkimresultstr = "softfail"; } + case 3 { $dkimresultstr = "neutral"; } + case 4 { $dkimresultstr = "temperror"; } + case 5 { $dkimresultstr = "permerror"; } + case 6 { $dkimresultstr = "none"; } + case 7 { $dkimresultstr = "fail"; } + case 8 { $dkimresultstr = "policy"; } + case 9 { $dkimresultstr = "nxdomain"; } + case 10 { $dkimresultstr = "signed"; } + case 12 { $dkimresultstr = "discard"; } + else { $dkimresultstr = "unknown"; } + } + + print $tmpout " \n"; + print $tmpout " $dkimdomain\n"; + print $tmpout " $dkimresultstr\n"; + print $tmpout " \n"; + } + + $dbi_d->finish; + + print $tmpout " \n"; + print $tmpout " \n"; + } + + $dbi_s->finish; + + print $tmpout "
\n"; + + close($tmpout); + + if ($rowcount == 0) + { + if ($verbose >= 2) + { + print STDERR "$progname: no activity selected for $domain; skipping\n"; + } + + unlink($repfile); + next; + } + + # zip the report + if (!zip [ $repfile ] => $zipfile) + { + print STDERR "$progname: can't zip report for domain $domain: $!\n"; + next; + } + + if ($keepfiles) + { + print STDERR "$progname: keeping report file \"$repfile\"\n"; + } + + # decode the URI + @repuris = split(',', $repuri); + + for $repuri (@repuris) + { + $uri = URI->new($repuri); + if (!defined($uri) || + !defined($uri->scheme) || + $uri->opaque eq "") + { + print STDERR "$progname: can't parse reporting URI for domain $domain\n"; + unlink($zipfile); + unlink($repfile); + next; + } + + $repdest = $uri->opaque; + my $report_maxbytes = $report_maxbytes_global; + + # check for max report size + if ($repdest =~ m/^(\S+)!(\d{1,15})([kmgt])?$/i) + { + $repdest = $1; + $report_maxbytes = $2; + if ($3) + { + my $letter = lc($3); + if ($letter eq 'k') + { + $report_maxbytes = $report_maxbytes * 1024; + } + if ($letter eq 'm') + { + $report_maxbytes = $report_maxbytes * 1048576; + } + if ($letter eq 'g') + { + $report_maxbytes = $report_maxbytes * (2**30); + } + if ($letter eq 't') + { + $report_maxbytes = $report_maxbytes * (2**40); + } + } + } + + # Test mode, just report what would have been done + if ($testmode) + { + print STDERR "$progname: would email $domain report for " . + "$rowcount records to " . $uri->opaque . "\n"; + } + # ensure a scheme is present + elsif (!defined($uri->scheme)) + { + if ($verbose >= 2) + { + print STDERR "$progname: unknown URI scheme in '$repuri' for domain $domain\n"; + } + + unlink($zipfile); + unlink($repfile); + next; + } + # send/post report + elsif ($uri->scheme eq "mailto") + { + my $datestr; + my $report_id; + + if (!open($zipin, $zipfile)) + { + print STDERR "$progname: can't read zipped report for $domain: $!\n"; + unlink($zipfile); + unlink($repfile); + next; + } + + $boundary = "report_section"; + + $report_id = $domain . "-" . $now . "@" . $repdom; + $datestr = strftime("%a, %e %b %Y %H:%M:%S %z (%Z)", + localtime); + + $mailout = "To: $repdest\n"; + $mailout .= "From: $repemail\n"; + $mailout .= "Subject: Report Domain: " . $domain . " Submitter: " . $repdom . " Report-ID: " . $report_id . "\n"; + $mailout .= "X-Mailer: " . $progname . " v" . $version ."\n"; + $mailout .= "Date: " . $datestr . "\n"; + $mailout .= "Message-ID: <$report_id>\n"; + $mailout .= "Auto-Submitted: auto-generated\n"; + $mailout .= "MIME-Version: 1.0\n"; + $mailout .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\n"; + $mailout .= "\n"; + $mailout .= "This is a MIME-encapsulated message.\n"; + $mailout .= "\n"; + $mailout .= "--$boundary\n"; + $mailout .= "Content-Type: text/plain;\n"; + $mailout .= "\n"; + $mailout .= "This is a DMARC aggregate report for $domain\n"; + $mailout .= "generated at " . localtime() . "\n"; + $mailout .= "\n"; + $mailout .= "--$boundary\n"; + $mailout .= "Content-Type: application/zip\n"; + $mailout .= "Content-Disposition: attachment; filename=\"$zipfile\"\n"; + $mailout .= "Content-Transfer-Encoding: base64\n"; + $mailout .= "\n"; + + while (read($zipin, $buf, 60*57)) + { + $mailout .= encode_base64($buf); + } + + $mailout .= "\n"; + $mailout .= "--$boundary--\n"; + my $reportsize = length($mailout); + + if ($reportsize > $report_maxbytes) + { + # XXX -- generate an error report here + print STDERR "$progname: report was too large ($reportsize bytes) per limitation of URI " . $uri->opaque . " for domain $domain\n"; + } + else + { + $smtpstatus = "sent"; + $smtpfail = 0; + if (!$smtp->mail($repemail) || + !$smtp->to($repdest) || + !$smtp->data() || + !$smtp->datasend($mailout) || + !$smtp->dataend()) + { + $smtpfail = 1; + $smtpstatus = "failed to send"; + } + + if ($verbose || $smtpfail) + { + # now perl voodoo: + $answer = ${${*$smtp}{'net_cmd_resp'}}[1]; + chomp($answer); + print STDERR "$progname: $smtpstatus report for $domain to $repdest ($answer)\n"; + } + } + + $smtp->reset(); + + close($zipin); + } + else + { + print STDERR "$progname: unsupported reporting URI scheme " . $uri->scheme . " for domain $domain\n"; + unlink($zipfile); + unlink($repfile); + next; + } + } + + # update "last sent" timestamp + if ($doupdate) + { + $dbi_s = $dbi_h->prepare("UPDATE requests SET lastsent = FROM_UNIXTIME(?) WHERE domain = ?"); + if (!$dbi_s->execute($now, $domainid)) + { + print STDERR "$progname: can't update last sent time for domain $domain: " . $dbi_h->errstr . "\n"; + $dbi_s->finish; + $dbi_h->disconnect; + exit(1); + } + } + + unlink($zipfile); + if (!$keepfiles) + { + unlink($repfile); + } +} + +$smtp->quit(); + +# +# all done! +# + +$dbi_s->finish; + +if ($verbose) +{ + print STDERR "$progname: terminating at " . localtime() . "\n"; +} + +$dbi_h->disconnect; + +exit(0); diff --git a/usr/share/doc/opendmarc/README.Debian b/usr/share/doc/opendmarc/README.Debian new file mode 100644 index 0000000..384fc8b --- /dev/null +++ b/usr/share/doc/opendmarc/README.Debian @@ -0,0 +1,48 @@ +opendmarc for Debian +------------------- + +Configuration Notes for Debian systes +-------------------------------------------- + +The DMARC protocol is built on top of SPF and DKIM. OpenDMARC needs SPF and +DKIM verification results as an input. OpenDMARC uses RFC 5451 Authentication +Results header fields to get those results. OpenDMARC will use header fields +with an AuthservID that matches either the one specified in +/etc/opendmarc.conf or the system hostname. It is important to verify that +the AuthservID provided by SPF and DKIM verifiers matches the one that +opendmarc expects. + +In Debian, postfix-policyd-spf-python and opendkim have been tested to +generate appropriate A-R header fields. For postfix-policyd-spf-python, +however, it is not the default configuration. See man 5 policyd-spf.conf for +information on how to configure it to generate A-R header fields. + +To generate aggregate feedback reports a MySQL database is needed. See the +man pages for opendmarc-expire, opendmarc-import, opendmarc-params, and +opendmarc-reports for details on how the aggregate report data collection and +report generation works. The database schema, setup script, and README.schema +files can be found in /usr/share/doc/opendmarc. + +Notes for Postfix users +----------------------- + +Postfix users who wish to access the opendmarc service via UNIX socket +may need to add the postfix user to the opendmarc group and ensure that +UMask is set to 002 in /etc/opendkim.conf, in order to make the socket + readable by Posfix. + +Users may also need to move the socket into a directory accessible by the +Postfix chroot; this can be accomplished by setting the SOCKET variable +in /etc/default/opendmarc. + +The default is to connect to the filter over TCP. The filter can be bound to +localhost to prevent other hosts from accessing it. For example, to bind to +port 8892, specify "inet:8892@localhost". + +Changing group ownership of socket +---------------------------------- + +The group ID of the UNIX socket created by opendkim can be changed by +changing the primary GID of the opendmarc user, e.g.: +$ usermod -g mail opendmarc + diff --git a/usr/share/doc/opendmarc/README.schema b/usr/share/doc/opendmarc/README.schema new file mode 100644 index 0000000..0f983d2 --- /dev/null +++ b/usr/share/doc/opendmarc/README.schema @@ -0,0 +1,39 @@ +This directory contains the OpenDMARC schema plus any related files. + +The tables in this schema are populated by the opendmarc filter as it processes +messages and downloads policies. The rows are then consumed by the scripts +in the "reports" directory to generate regular aggregate reports. + +The tables are summarized here: + +domains A table that maps domain names to unique integer IDs. + Automatically tracks a "first seen" timestamp, and includes + a column to record when the last report was sent. + +reporters A table mapping reporting hosts to unique integer IDs. + Intended for use by multi-MX systems so it's possible to tell + where an inbound message landed. + +ipaddr A table mapping IP addresses (as strings) to unique IDs. + Also tracks the "first seen" timestamp for each. + +messages A table tracking salient properties of all messages received. + A messages is uniquely identified by a {date, jobid, reporter} + tuple. Includes references to the "domains" table to track + the RFC5321.MailFrom domain, the RFC5322.From domain. + Also records the count of DKIM signatures, the SPF result, + and whether or not the SPF result was aligned with the + RFC5322.From domain. + +signatures A table tracking DKIM signatures, each of which refers to + a rown in the "messages" table. Tracks the signing domain, + whether the signature passed, whether there was a verification + error other than a broken signature, and whether or not the + signing domain aligned with the RFC5322.From domain. + +requests A table containing a cache of DMARC reporting requests. + For each domain, the destination reporting URI for aggregate + reports is recorded along with a "last report sent" timestamp. + +-- +Copyright (c) 2012, The Trusted Domain Project. All rights reserved. diff --git a/usr/share/doc/opendmarc/copyright b/usr/share/doc/opendmarc/copyright new file mode 100644 index 0000000..58d97f6 --- /dev/null +++ b/usr/share/doc/opendmarc/copyright @@ -0,0 +1,170 @@ +This package was debianized by Scott Kitterman on +Tue, 30 Oct 2012 14:46:53 +0100. + +It was downloaded from http://sourceforge.net/projects/opendkim + +Copyright Holder: The OpenDKIM Project. + +Based on code from DKIM Milter, copyright Sendmail Inc. + +Copyright: +Copyright (c) 2009, 2010, 2012, 2013, 2014 The Trusted Domain Project. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of The Trusted Domain Project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +Portions of this project are also covered by the Sendmail Open Source +License, available in this distribution in the file "LICENSE.Sendmail". +See the copyright notice(s) in each file to determine whether it is covered +by either or both of the licenses. For example: + + Copyright (c) Sendmail, Inc. and its suppliers. + All rights reserved. + +Files bearing the banner above are covered under the Sendmail Open Source +License (see LICENSE.Sendmail). + + Copyright (c) , The Trusted Domain Project. + All rights reserved. + +Files bearing the banner above are covered under the Trusted Domain Project +License (above). + +Files bearing both banners are covered under both sets of license terms. + +THIS SOFTWARE IS PROVIDED BY THE TRUSTED DOMAIN PROJECT ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE TRUSTED DOMAIN PROJECT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +For files: +opendmarc/parse.h Copyright (c) 2004 Sendmail, Inc. and its suppliers. +opendmarc/opendmarc-ar.c Copyright (c) 2007-2009 Sendmail, Inc. and its suppliers. +opendmarc/opendmarc-dstring.c Copyright (c) 2005-2009 Sendmail, Inc. and its suppliers. +opendmarc/opendmarc-dstring.h Copyright (c) 2004, 2005, 2007-2009 Sendmail, Inc. and its suppliers. +opendmarc/opendmarc-ar.h Copyright (c) 2007-2009 Sendmail, Inc. and its suppliers. +opendmarc/config.c Copyright (c) 2006-2009 Sendmail, Inc. and its suppliers. +opendmarc/parse.c Copyright (c) 2005, 2007, 2008 Sendmail, Inc. and its suppliers. +opendmarc/config.h Copyright (c) 2006-2008 Sendmail, Inc. and its suppliers + + SENDMAIL OPEN SOURCE LICENSE + +The following license terms and conditions apply to this open source +software ("Software"), unless a different license is obtained directly +from Sendmail, Inc. ("Sendmail") located at 6475 Christie Ave, Suite 350, +Emeryville, CA 94608, USA. + +Use, modification and redistribution (including distribution of any +modified or derived work) of the Software in source and binary forms is +permitted only if each of the following conditions of 1-6 are met: + +1. Redistributions of the Software qualify as "freeware" or "open + source software" under one of the following terms: + + (a) Redistributions are made at no charge beyond the reasonable + cost of materials and delivery; or + + (b) Redistributions are accompanied by a copy of the modified + Source Code (on an acceptable machine-readable medium) or by an + irrevocable offer to provide a copy of the modified Source Code + (on an acceptable machine-readable medium) for up to three years + at the cost of materials and delivery. Such redistributions must + allow further use, modification, and redistribution of the Source + Code under substantially the same terms as this license. For + the purposes of redistribution "Source Code" means the complete + human-readable, compilable, linkable, and operational source + code of the redistributed module(s) including all modifications. + +2. Redistributions of the Software Source Code must retain the + copyright notices as they appear in each Source Code file, these + license terms and conditions, and the disclaimer/limitation of + liability set forth in paragraph 6 below. Redistributions of the + Software Source Code must also comply with the copyright notices + and/or license terms and conditions imposed by contributors on + embedded code. The contributors' license terms and conditions + and/or copyright notices are contained in the Source Code + distribution. + +3. Redistributions of the Software in binary form must reproduce the + Copyright Notice described below, these license terms and conditions, + and the disclaimer/limitation of liability set forth in paragraph + 6 below, in the documentation and/or other materials provided with + the binary distribution. For the purposes of binary distribution, + "Copyright Notice" refers to the following language: "Copyright (c) + 1998-2009 Sendmail, Inc. All rights reserved." + +4. Neither the name, trademark or logo of Sendmail, Inc. (including + without limitation its subsidiaries or affiliates) or its contributors + may be used to endorse or promote products, or software or services + derived from this Software without specific prior written permission. + The name "sendmail" is a registered trademark and service mark of + Sendmail, Inc. + +5. We reserve the right to cancel this license if you do not comply with + the terms. This license is governed by California law and both of us + agree that for any dispute arising out of or relating to this Software, + that jurisdiction and venue is proper in San Francisco or Alameda + counties. These license terms and conditions reflect the complete + agreement for the license of the Software (which means this supercedes + prior or contemporaneous agreements or representations). If any term + or condition under this license is found to be invalid, the remaining + terms and conditions still apply. + +6. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY + SENDMAIL AND ITS CONTRIBUTORS "AS IS" WITHOUT WARRANTY OF ANY KIND + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT AND FITNESS FOR A + PARTICULAR PURPOSE ARE EXPRESSLY DISCLAIMED. IN NO EVENT SHALL SENDMAIL + OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + WITHOUT LIMITATION NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +$Revision: 1.1 $ $Date: 2009/07/16 18:43:18 $ + +For file contrib/rddmarc/dmarcfail.py: + +# Copyright 2012, Taughannock Networks. All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + diff --git a/usr/share/doc/opendmarc/mkdb.mysql b/usr/share/doc/opendmarc/mkdb.mysql new file mode 100644 index 0000000..83f5de5 --- /dev/null +++ b/usr/share/doc/opendmarc/mkdb.mysql @@ -0,0 +1,77 @@ +-- Copyright (c) 2013, The Trusted Domain Project. All rights reserved. + +-- MySQL command sequence to create a database to accumulate OpenDMARC +-- report data + +-- table mapping domain names to id numbers +CREATE TABLE domains ( + id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(id), + UNIQUE KEY(name) +) ENGINE=innodb DEFAULT CHARSET=latin1; + +-- table mapping IP addresses to id numbers +CREATE TABLE ipaddr ( + id INT(11) NOT NULL AUTO_INCREMENT, + addr VARCHAR(64) DEFAULT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(id), + UNIQUE KEY(addr) +) ENGINE=innodb DEFAULT CHARSET=latin1; + +-- table tracking message-specific data +CREATE TABLE messages ( + id INT(11) NOT NULL AUTO_INCREMENT, + date TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, + jobid VARCHAR(128) NOT NULL, + reporter INT(10) UNSIGNED NOT NULL, + ip INT(10) UNSIGNED NOT NULL, + policy TINYINT(3) UNSIGNED NOT NULL, + disp TINYINT(3) UNSIGNED NOT NULL, + from_domain INT(10) UNSIGNED NOT NULL, + env_domain INT(10) UNSIGNED NOT NULL, + policy_domain INT(10) UNSIGNED NOT NULL, + sigcount TINYINT(3) UNSIGNED NOT NULL, + spf TINYINT(3) NOT NULL, + align_spf TINYINT(3) UNSIGNED NOT NULL, + align_dkim TINYINT(3) UNSIGNED NOT NULL, + PRIMARY KEY(id), + UNIQUE KEY(reporter,date,jobid) +) ENGINE=innodb DEFAULT CHARSET=latin1; + +-- table mapping reporters to ids +CREATE TABLE reporters ( + id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(id), + UNIQUE KEY(name) +) ENGINE=innodb DEFAULT CHARSET=latin1; + +-- table tracking report requests +CREATE TABLE requests ( + id INT(11) NOT NULL AUTO_INCREMENT, + domain INT(11) NOT NULL, + repuri VARCHAR(255) NOT NULL, + pct TINYINT(4) NOT NULL, + policy TINYINT(4) NOT NULL, + spolicy TINYINT(4) NOT NULL, + aspf TINYINT(4) NOT NULL, + adkim TINYINT(4) NOT NULL, + locked TINYINT(4) NOT NULL DEFAULT '0', + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + lastsent TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(id) +) ENGINE=innodb DEFAULT CHARSET=latin1; + +-- table for tracking DKIM signature evaluation results +CREATE TABLE signatures ( + id INT(11) NOT NULL AUTO_INCREMENT, + message INT(11) NOT NULL, + domain INT(11) NOT NULL, + pass TINYINT(4) NOT NULL, + error TINYINT(4) NOT NULL, + PRIMARY KEY(id) +) ENGINE=innodb DEFAULT CHARSET=latin1; diff --git a/usr/share/doc/opendmarc/schema.mysql b/usr/share/doc/opendmarc/schema.mysql new file mode 100644 index 0000000..3f878cb --- /dev/null +++ b/usr/share/doc/opendmarc/schema.mysql @@ -0,0 +1,93 @@ +-- OpenDMARC database schema +-- +-- Copyright (c) 2012, The Trusted Domain Project. +-- All rights reserved. + +CREATE DATABASE IF NOT EXISTS opendmarc; +USE opendmarc; + +-- A table for mapping domain names and their DMARC policies to IDs +CREATE TABLE IF NOT EXISTS domains ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY(id), + UNIQUE KEY(name) +); + +-- A table for logging reporting requests +CREATE TABLE IF NOT EXISTS requests ( + id INT NOT NULL AUTO_INCREMENT, + domain INT NOT NULL, + repuri VARCHAR(255) NOT NULL, + adkim TINYINT NOT NULL, + aspf TINYINT NOT NULL, + policy TINYINT NOT NULL, + spolicy TINYINT NOT NULL, + pct TINYINT NOT NULL, + locked TINYINT NOT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + lastsent TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + + PRIMARY KEY(id), + KEY(lastsent), + UNIQUE KEY(domain) +); + +-- A table for reporting hosts +CREATE TABLE IF NOT EXISTS reporters ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY(id), + UNIQUE KEY(name) +); + +-- A table for IP addresses +CREATE TABLE IF NOT EXISTS ipaddr ( + id INT NOT NULL AUTO_INCREMENT, + addr VARCHAR(64) NOT NULL, + firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY(id), + UNIQUE KEY(addr) +); + +-- A table for messages +CREATE TABLE IF NOT EXISTS messages ( + id INT NOT NULL AUTO_INCREMENT, + date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + jobid VARCHAR(128) NOT NULL, + reporter INT UNSIGNED NOT NULL, + policy TINYINT UNSIGNED NOT NULL, + disp TINYINT UNSIGNED NOT NULL, + ip INT UNSIGNED NOT NULL, + env_domain INT UNSIGNED NOT NULL, + from_domain INT UNSIGNED NOT NULL, + policy_domain INT UNSIGNED NOT NULL, + spf TINYINT UNSIGNED NOT NULL, + align_dkim TINYINT UNSIGNED NOT NULL, + align_spf TINYINT UNSIGNED NOT NULL, + sigcount TINYINT UNSIGNED NOT NULL, + + PRIMARY KEY(id), + KEY(date), + UNIQUE KEY(reporter, date, jobid) +); + +-- A table for signatures +CREATE TABLE IF NOT EXISTS signatures ( + id INT NOT NULL AUTO_INCREMENT, + message INT NOT NULL, + domain INT NOT NULL, + pass TINYINT NOT NULL, + error TINYINT NOT NULL, + + PRIMARY KEY(id), + KEY(message) +); + +-- CREATE USER 'opendmarc'@'localhost' IDENTIFIED BY 'changeme'; +-- GRANT ALL ON opendmarc.* to 'opendmarc'@'localhost'; -- 2.34.1