Ensure that message boundaries generated by mhsign are ascii-only
[mmh] / uip / mhsign.sh
1 #!/bin/sh
2 # Based on mhsign 1.1.0.9 2007/05/30 14:48:40 by Neil Rickert
3 # Adjusted to mmh by markus schnalke <meillo@marmaro.de>, 2012-07
4
5
6 # mhsign:
7 #   -encrypt:  Encrypt to recipients of message. This implies signing.
8 #   -mime:     Use MIME pgp standard.  For signature, trailing blanks
9 #              will be removed and any "From " line will be indented for
10 #              best compatibility. Enforced for multipart messages.
11
12 usage="Usage: mhsign [-encrypt] [-mime] [-Version] [-help] file"
13
14 # defaults
15 usemime=n
16 function=sign
17
18
19 # find out the signing key
20 userid="$MMHPGPKEY"
21 if [ -z "$userid" ] ; then
22         userid="`mhparam pgpkey`"
23 fi
24 if [ -z "$userid" ] ; then
25         userid="`gpg --list-secret-keys --with-colons --fixed-list-mode \
26                                 2>/dev/null |
27                         grep '^sec' | sort -t: -k3,3nr -k 6,6nr |
28                         awk -F: '
29                                 $7=="" || $7 > "'"\`date +%s\`"'" {
30                                         print $5; exit;
31                                 }
32                         '`"
33 fi
34 if [ -z "$userid" ] ; then
35         echo "No secret key found" >&2
36         exit 1
37 fi
38
39 # find out the file of recipient key exceptions (for encrypt only)
40 keyfile="${MMH:-$HOME/.mmh}/pgpkeys"
41 if [ ! -r "$keyfile" ] ; then
42         keyfile="${GNUPGHOME:-$HOME/.gnupg}/pgpkeys"
43         if [ ! -r "$keyfile" ] ; then
44                 keyfile=/dev/null
45         fi
46 fi
47
48 # prepend the default options from the profile
49 set -- `mhparam -nocomp ${0##*/}` "$@"
50
51 while : ; do
52         case "$1" in
53         -e*)
54                 function=encrypt
55                 ;;
56         -m*)
57                 usemime=y
58                 ;;
59         -V*)
60                 echo "mhsign has no own version number, thus this instead:"
61                 folder -Version
62                 exit 0
63                 ;;
64         -h*|-*)
65                 echo "$usage" >&2
66                 exit 1
67                 ;;
68         *)
69                 break
70         esac
71         shift
72 done
73
74 if [ $# -ne 1 ] ; then
75         echo "$usage" >&2
76         exit 1
77 fi
78
79 TEMP=/tmp/${0##*/}.$$
80 umask 077
81 mkdir $TEMP || exit 1
82 trap "rm -rf $TEMP" 0 1 2 15
83
84 ### lookupkeyfile address -- lookup one address in our database
85 lookupkeyfile() {
86         key=`grep -i "^[^#].*[  ]$1\$" "$keyfile" 2>/dev/null`
87         if [ $? != 0 ] ; then
88                 return 1
89         fi
90         echo "$key" | sed 's/[  ].*//;q'
91         return 0
92 }
93
94 ### lookupkeyring address -- lookup one address in keyring
95 lookupkeyring() {
96         key=`gpg --list-keys --with-colons "<$1>" 2>/dev/null`
97         if [ $? != 0 ] ; then
98                 return 1
99         fi
100         echo "$key" | sed -n '/^pub:[^idre]:/{p;q;}' | cut -d: -f5
101         return 0
102 }
103
104 ### Do a best guess at FQDN
105 mh_hostname()
106 {
107         hostname -f 2>/dev/null || uname -n
108 }
109
110 ### lookupkeys file -- set $KL to list of recipient keys
111 lookupkeys() {
112         KL=
113         status=0
114         if whom -ali -notocc -bcc "$1" >/dev/null ; then
115                 echo "Encryption is not supported for BCCs" >&2
116                 return 1
117         fi
118
119         # extract the actual address
120         format='%<{error}%{error}: %{text}%|%(addr{text})%>'
121         addresses=`whom -ali -tocc -nobcc "$1" |sed 's_$_,_'`
122         addresses=`%libdir%/ap -form "=$format" "$addresses"`
123
124         for i in $addresses ; do
125                 case "$i" in
126                 '|'*)   echo "Ignoring pipe address" >&2
127                         continue ;;
128                 *@*)    ;;
129                 *)      i="$i@`mh_hostname`" ;;
130                 esac
131                 if k=`lookupkeyfile "$i"` ; then
132                         KL="$KL $k"
133                 elif k=`lookupkeyring "$i"` ; then
134                         KL="$KL $k"
135                 else
136                         echo "Could not find key for <$i>" >&2
137                         status=1
138                 fi
139         done
140         return $status
141 }
142
143 ### getheader headername msgfile
144 getheader() {
145         HDR=`sed -n -e '/^-*$/q' -e 's/^\([^    :]*\):.*/\1/p' $2 |
146                 grep -i '^'"$1"'$' | head -1`
147         if [ "$HDR" = "" ] ; then return 1 ; fi
148         sed -n -e ':a
149                 /^-*$/q
150                 /^'"$HDR"':/b x
151                 d
152                 b a
153                 :x
154                 p
155                 n
156                 /^[     ]/b x
157                 b a' $2
158                 return 0
159 }
160
161 ### headbody msgfile # separate msgfile into $TEMP/head $TEMP/body
162 headbody() {
163         sed -n '1,/^\-*$/p' "$1" > $TEMP/head
164         sed '1,/^-*$/d' "$1" > $TEMP/body
165 }
166
167 ### fixheaders -- remove Content headers, add newheaders
168 fixheaders() {
169         sed -n ':a
170                 /^-*$/q
171                 /^[Cc][Oo][Nn][Tt][Ee][Nn][Tt]-/b r
172                 p
173                 n
174                 b a
175                 :r
176                 n
177                 /^[     ]/b r
178                 b a' $TEMP/head
179         cat $TEMP/newheaders
180         grep "^-" $TEMP/head || echo ""
181 }
182
183 ### newboundary -- output a suitable boundary marker
184 newboundary() {
185         b=$$_`LC_ALL=C date|sed 's/[ :  ]/_/g'`
186         for i in 0 x '=' _ + , Z 9 4 ; do
187                 if grep "^--$b" $TEMP/body >/dev/null 2>&1 ; then
188                         ## oops, bad boundary -- try again
189                         b=`echo $i$b | tr \
190 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456780=+,_' \
191 '3Ba+c98bdACmzXpqR,tTuMDSs_hLkwZ0ef7PQrW=2x5l6E14ZKivIVgOjoJnGNUyHF'`
192                 else
193                         echo "$b"
194                         return 0
195                 fi
196         done
197         echo "Failed to generate unique mime boundary" >&2
198         exit 1
199 }
200
201 ### detachsign -- sign $TEMP/body, output in $TEMP/body.asc
202 detachsign() {
203         gpg -u "$userid" --armor --textmode --detach-sign \
204                         <$TEMP/body >$TEMP/body.asc
205 }
206
207 ### sign --- inline signature for $TEMP/body, output in $TEMP/body.asc
208 sign() {
209         gpg -u "$userid" --armor --textmode --clearsign \
210                 <$TEMP/body >$TEMP/body.asc
211 }
212
213 ### encrypt recipients -- encrypt $TEMP/body to recipients
214 encrypt() {
215         R=
216         for i in $KL ; do
217                 R="$R -r $i"
218         done
219         gpg --no-encrypt-to -u "$userid" --armor --textmode \
220                         --always-trust --output $TEMP/body.asc \
221                         -r "$userid" $R --sign --encrypt $TEMP/body
222 }
223
224 ### Mainline processing
225
226 FILE="$1"       ## we assume a disk file
227 if [ ! -r "$FILE" ] ; then
228         echo "cannot read $FILE" >&2
229         exit 1
230 fi
231
232 case "$function" in
233 encrypt)
234         lookupkeys "$FILE" || exit 1
235 esac
236
237 cp "$FILE" "$FILE.orig"
238 outfile="$FILE"
239 headbody "$FILE"
240
241 CT=""
242 if grep -i "^mime-version:" $TEMP/head >/dev/null 2>&1 ; then
243         >$TEMP/newheaders
244         if CT=`getheader content-type $TEMP/head` ; then
245                 echo "$CT" >$TEMP/newbody
246                 if grep -i multipart $TEMP/newbody >/dev/null 2>&1 ; then
247                         usemime=y  # Force MIME if already multi-part
248                 fi
249                 getheader content-transfer-encoding $TEMP/head \
250                                 >>$TEMP/newbody || :
251         else
252                 CT=""
253         fi
254 else
255         echo "Mime-Version: 1.0" >$TEMP/newheaders
256 fi
257
258 if [ "$usemime" = n ] ; then
259         ### non-MIME ###
260         case "$function" in
261         sign)
262                 sign || exit 1 ;;
263         encrypt)
264                 encrypt || exit 1 ;;
265         esac
266         cat $TEMP/head $TEMP/body.asc >$outfile || exit 1
267         exit 0
268 fi
269
270 ### MIME ###
271
272 BDRY="`newboundary`"
273
274 if [ "$CT" = "" ] ; then
275         echo "Content-Type: text/plain; charset=us-ascii" >$TEMP/newbody
276 fi
277 echo >>$TEMP/newbody
278
279 case $function in
280 sign)
281         sed 's/^From / &/; s/[ \r        ]*$//' $TEMP/body >>$TEMP/newbody
282         if grep "^From " $TEMP/body >/dev/null 2>&1 ; then
283                 echo 'Warning: "From " lines in message body have been indented' >&2
284         fi
285         if grep "[ \r    ]$" $TEMP/body >/dev/null 2>&1 ; then
286                 echo 'Warning: trailing blanks removed from message body' >&2
287         fi
288         echo 'Content-Type: multipart/signed; protocol="application/pgp-signature";' >>$TEMP/newheaders
289         echo "  micalg=pgp-sha1"'; boundary="'"$BDRY"'"' >>$TEMP/newheaders
290
291         sed -e 's/$/\r/' "$TEMP/newbody" >"$TEMP/body"
292         detachsign || exit 1
293         (
294                 echo "--$BDRY"
295                 cat $TEMP/newbody
296                 echo
297                 echo "--$BDRY"
298                 echo "Content-Type: application/pgp-signature"
299                 echo
300                 cat $TEMP/body.asc
301                 echo
302                 echo "--$BDRY--"
303                 echo
304         ) >$TEMP/body
305         ;;
306
307 encrypt)
308         cat $TEMP/body >>$TEMP/newbody
309         echo 'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' >>$TEMP/newheaders
310         echo "  boundary=\"$BDRY\"" >> $TEMP/newheaders
311
312         mv $TEMP/newbody $TEMP/body || exit 1
313         encrypt || exit 1
314         (
315                 echo "--$BDRY"
316                 echo "Content-Type: application/pgp-encrypted"
317                 echo
318                 echo "Version: 1"
319                 echo
320                 echo "--$BDRY"
321                 echo "Content-Type: application/octet-stream"
322                 echo
323                 cat $TEMP/body.asc
324                 echo
325                 echo "--$BDRY--"
326                 echo
327         ) >"$TEMP/body"
328         ;;
329 esac
330
331 fixheaders | cat - $TEMP/body >"$outfile"