Merge pull request #1331 from k9mail/compose-mime-combined

PGP/INLINE and build in combined mime format
This commit is contained in:
Vincent 2016-05-13 14:44:40 +02:00
commit 94b9da3d16
45 changed files with 1925 additions and 271 deletions

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="457.47px"
height="457.469px"
viewBox="0 0 457.47 457.469"
style="enable-background:new 0 0 457.47 457.469;"
xml:space="preserve"
inkscape:version="0.48.5 r10040"
sodipodi:docname="circled_plus.svg"><metadata
id="metadata41"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs39" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1056"
id="namedview37"
showgrid="false"
inkscape:zoom="2.0635278"
inkscape:cx="212.63748"
inkscape:cy="200.65921"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="0"
inkscape:current-layer="Capa_1" /><g
id="g3"
transform="matrix(0.57627118,0,0,0.57627118,96.921399,96.921399)"><path
d="M 228.734,0 C 102.41,0 0,102.41 0,228.735 0,355.06 102.41,457.469 228.734,457.469 355.059,457.469 457.469,355.06 457.469,228.735 457.47,102.41 355.06,0 228.734,0 z m 130.534,265.476 -255.005,0 c -16.674,0 -30.192,-13.512 -30.192,-30.187 0,-16.674 13.518,-30.188 30.192,-30.188 l 255.005,0.005 c 16.669,0 30.192,13.515 30.192,30.188 0,16.676 -13.523,30.182 -30.192,30.182 z"
id="path5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssccsccsc" /></g><g
id="g7" /><g
id="g9" /><g
id="g11" /><g
id="g13" /><g
id="g15" /><g
id="g17" /><g
id="g19" /><g
id="g21" /><g
id="g23" /><g
id="g25" /><g
id="g27" /><g
id="g29" /><g
id="g31" /><g
id="g33" /><g
id="g35" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="457.47px"
height="457.469px"
viewBox="0 0 457.47 457.469"
style="enable-background:new 0 0 457.47 457.469;"
xml:space="preserve"
inkscape:version="0.48.5 r10040"
sodipodi:docname="circled_plus.svg"><metadata
id="metadata41"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs39" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1056"
id="namedview37"
showgrid="false"
inkscape:zoom="0.51588196"
inkscape:cx="228.735"
inkscape:cy="228.7345"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="0"
inkscape:current-layer="Capa_1" /><g
id="g3"
transform="matrix(0.57627118,0,0,0.57627118,96.921399,96.921399)"><path
d="M 228.734,0 C 102.41,0 0,102.41 0,228.735 0,355.06 102.41,457.469 228.734,457.469 355.059,457.469 457.469,355.06 457.469,228.735 457.47,102.41 355.06,0 228.734,0 z m 130.534,265.476 h -97.326 v 97.315 c 0,16.668 -13.506,30.186 -30.181,30.186 -16.668,0 -30.189,-13.518 -30.189,-30.186 v -97.315 h -97.309 c -16.674,0 -30.192,-13.512 -30.192,-30.187 0,-16.674 13.518,-30.188 30.192,-30.188 h 97.315 v -97.31 c 0,-16.674 13.515,-30.183 30.189,-30.183 16.675,0 30.187,13.509 30.187,30.183 v 97.315 h 97.314 c 16.669,0 30.192,13.515 30.192,30.188 0,16.676 -13.523,30.182 -30.192,30.182 z"
id="path5"
inkscape:connector-curvature="0" /></g><g
id="g7" /><g
id="g9" /><g
id="g11" /><g
id="g13" /><g
id="g15" /><g
id="g17" /><g
id="g19" /><g
id="g21" /><g
id="g23" /><g
id="g25" /><g
id="g27" /><g
id="g29" /><g
id="g31" /><g
id="g33" /><g
id="g35" /></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17.73 12.02l3.98-3.98c.39-.39.39-1.02 0-1.41l-4.34-4.34c-.39-.39-1.02-.39-1.41 0l-3.98 3.98L8 2.29C7.8 2.1 7.55 2 7.29 2c-.25 0-.51.1-.7.29L2.25 6.63c-.39.39-.39 1.02 0 1.41l3.98 3.98L2.25 16c-.39.39-.39 1.02 0 1.41l4.34 4.34c.39.39 1.02.39 1.41 0l3.98-3.98 3.98 3.98c.2.2.45.29.71.29.26 0 .51-.1.71-.29l4.34-4.34c.39-.39.39-1.02 0-1.41l-3.99-3.98zM12 9c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm-4.71 1.96L3.66 7.34l3.63-3.63 3.62 3.62-3.62 3.63zM10 13c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm2 2c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm2-4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2.66 9.34l-3.63-3.62 3.63-3.63 3.62 3.62-3.62 3.63z"/></svg>

After

Width:  |  Height:  |  Size: 765 B

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="100px"
height="100px"
viewBox="0 0 100 100"
version="1.1"
id="svg2">
<metadata
id="metadata16">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>lock-closed</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title
id="title4">lock-closed</title>
<desc
id="desc6">Created with Sketch.</desc>
<defs
id="defs8" />
<g
id="Page-1"
sketch:type="MSPage"
stroke-width="1"
stroke="none"
fill-rule="evenodd"
fill="none">
<g
id="lock-closed"
sketch:type="MSArtboardGroup"
fill="#000000">
<path
style="fill:#bbbbbb;fill-opacity:1"
id="path12"
d="M 50.476562 -0.1640625 C 39.10279 -0.1640625 28.584583 5.4442406 23.6875 15.470703 L 34.507812 26.484375 C 35.673202 18.087003 41.432062 11.783203 50.183594 11.783203 C 59.924594 11.783203 66.087891 18.740547 66.087891 29.685547 L 66.097656 45.132812 L 52.826172 45.132812 L 90.181641 83.160156 L 90.181641 54.564453 C 90.182641 46.109453 81.726953 45.132813 81.501953 45.132812 L 79.576172 45.132812 L 79.576172 29.478516 C 79.478172 10.284516 66.387565 -0.1640625 50.476562 -0.1640625 z M 20.800781 27.681641 C 20.786221 28.272513 20.784637 28.871176 20.800781 29.478516 L 20.785156 45.111328 C 20.785156 45.111328 21.024219 45.132812 19.824219 45.132812 C 18.554219 45.132812 10.185547 46.605359 10.185547 54.068359 L 10.185547 89.892578 C 10.185547 97.851578 19.604219 99.835935 19.824219 99.835938 L 81.027344 99.835938 C 81.186913 99.835938 85.917626 99.295803 88.511719 95.59375 L 38.199219 45.132812 L 34.261719 45.132812 L 34.265625 41.1875 L 20.800781 27.681641 z " />
</g>
</g>
<path
id="path4284"
d="m 9.9914329,20.473123 4.7937031,-4.407849 79.06095,80.479204 -4.010925,4.009432 z"
style="fill:#bbbbbb;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="svg3344"
viewBox="0 0 149.99999 99.999998"
height="100"
width="150">
<defs
id="defs3346" />
<metadata
id="metadata3349">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="fill:#669900;fill-opacity:1"
transform="translate(-17.144857,-22.362199)"
id="layer1">
<circle
r="13"
cy="106.8622"
cx="138.57143"
id="circle4219"
style="opacity:1;fill:#669900;fill-opacity:1;stroke:#669900;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle
style="opacity:1;fill:#669900;fill-opacity:1;stroke:#669900;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle4741"
cx="138.57143"
cy="72.362198"
r="13" />
<circle
r="13"
cy="37.862198"
cx="138.57143"
id="circle4743"
style="opacity:1;fill:#669900;fill-opacity:1;stroke:#669900;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
transform="translate(17.144849,22.526199)"
style="fill:#669900;fill-rule:evenodd;stroke:none;stroke-width:1;fill-opacity:1"
sketch:type="MSPage"
id="Page-1">
<g
style="fill:#669900;fill-opacity:1"
sketch:type="MSArtboardGroup"
id="lock-closed">
<path
style="fill:#669900;fill-opacity:1"
id="path12"
sketch:type="MSShapeGroup"
d="m 81.502,45.132 -1.925,0 0,-15.653 C 79.479,10.285 66.387,-0.164 50.476,-0.164 34.57,-0.164 20.304,10.782 20.801,29.479 l -0.016,15.633 c 0,0 0.24,0.021 -0.96,0.021 -1.27,0 -9.64,1.473 -9.64,8.936 l 0,35.824 c 0,7.959 9.42,9.943 9.64,9.943 l 61.202,0 c 0.22,0 9.154,-0.993 9.154,-9.943 l 0,-35.329 c 0.001,-8.455 -8.454,-9.432 -8.679,-9.432 z m -47.241,0 0.016,-15.446 c 0,-9.949 6.071,-17.903 15.906,-17.903 9.741,0 15.905,6.958 15.905,17.903 l 0.01,15.446 -31.837,0 z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="150"
height="100"
viewBox="0 0 149.99999 99.999998"
id="svg3344"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="signcrypt_error.svg">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="784"
id="namedview12"
showgrid="false"
inkscape:zoom="2.36"
inkscape:cx="11.016949"
inkscape:cy="50"
inkscape:window-x="0"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs3346" />
<metadata
id="metadata3349">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-17.144857,-22.362199)"
style="fill:#669900;fill-opacity:1">
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#bbbbbb;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle4219"
cx="138.57143"
cy="106.8622"
r="13" />
<circle
r="13"
cy="72.362198"
cx="138.57143"
id="circle4741"
style="opacity:1;fill:none;fill-opacity:1;stroke:#bbbbbb;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#bbbbbb;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle4743"
cx="138.57143"
cy="37.862198"
r="13" />
<g
transform="translate(18.184849,22.191199)"
style="fill:#cc0000;fill-rule:evenodd;stroke:none;stroke-width:1;fill-opacity:1"
id="Page-1"
sketch:type="MSPage">
<g
style="fill:#cc0000;fill-opacity:1"
id="lock-error"
sketch:type="MSArtboardGroup">
<path
d="m 80.459,45.474 -1.926,0 0,-15.648 C 78.435,10.633 65.344,0.183 49.433,0.183 33.527,0.183 19.265,11.128 19.761,29.826 l -0.016,15.628 c 0,0 0.24,0.021 -0.961,0.021 -1.27,0 -9.639,1.471 -9.639,8.932 l 0,35.821 c 0,7.959 9.42,9.943 9.639,9.943 l 61.2,0 c 0.219,0 9.154,-0.993 9.154,-9.943 l 0,-35.327 c 10e-4,-8.449 -8.454,-9.427 -8.679,-9.427 l 0,0 z M 33.234,30.033 c 0,-9.949 6.07,-17.902 15.906,-17.902 9.741,0 15.905,6.957 15.905,17.902 l 0.01,15.441 -31.837,0 0.016,-15.441 0,0 z M 59.403377,90.873 48.458282,79.927905 38.229659,90.364449 31.6365,83.756888 42.582495,72.815394 32.343971,62.593972 38.754412,56.107923 49.701306,67.050318 60.123449,56.710084 66.6365,63.224035 55.689605,74.167329 66.009137,84.477861 59.403377,90.873 Z"
sketch:type="MSShapeGroup"
id="path5450"
style="fill:#cc0000;fill-opacity:1" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="svg3344"
viewBox="0 0 149.99999 99.999998"
height="100"
width="150">
<defs
id="defs3346" />
<metadata
id="metadata3349">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="fill:#669900;fill-opacity:1"
transform="translate(-17.144857,-22.362199)"
id="layer1">
<circle
r="13"
cy="106.8622"
cx="138.57143"
id="circle4219"
style="opacity:1;fill:#ff8800;fill-opacity:1;stroke:#ff8800;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle
style="opacity:1;fill:#ff8800;fill-opacity:1;stroke:#ff8800;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle4741"
cx="138.57143"
cy="72.362198"
r="13" />
<circle
r="13"
cy="37.862198"
cx="138.57143"
id="circle4743"
style="opacity:1;fill:none;fill-opacity:1;stroke:#bbbbbb;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
transform="translate(17.144849,22.526199)"
style="fill:#ff8800;fill-rule:evenodd;stroke:none;stroke-width:1;fill-opacity:1"
sketch:type="MSPage"
id="Page-1">
<g
style="fill:#ff8800;fill-opacity:1"
sketch:type="MSArtboardGroup"
id="lock-closed">
<path
style="fill:#ff8800;fill-opacity:1"
id="path12"
sketch:type="MSShapeGroup"
d="m 81.502,45.132 -1.925,0 0,-15.653 C 79.479,10.285 66.387,-0.164 50.476,-0.164 34.57,-0.164 20.304,10.782 20.801,29.479 l -0.016,15.633 c 0,0 0.24,0.021 -0.96,0.021 -1.27,0 -9.64,1.473 -9.64,8.936 l 0,35.824 c 0,7.959 9.42,9.943 9.64,9.943 l 61.202,0 c 0.22,0 9.154,-0.993 9.154,-9.943 l 0,-35.329 c 0.001,-8.455 -8.454,-9.432 -8.679,-9.432 z m -47.241,0 0.016,-15.446 c 0,-9.949 6.071,-17.903 15.906,-17.903 9.741,0 15.905,6.958 15.905,17.903 l 0.01,15.446 -31.837,0 z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg3344"
viewBox="0 0 149.99999 99.999998"
height="100"
width="150"
inkscape:version="0.91 r13725"
sodipodi:docname="signcrypt_unknown.svg">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="784"
id="namedview10"
showgrid="false"
inkscape:zoom="2.36"
inkscape:cx="63.771186"
inkscape:cy="50"
inkscape:window-x="0"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs3346" />
<metadata
id="metadata3349">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="fill:#669900;fill-opacity:1"
transform="translate(-17.144857,-22.362199)"
id="layer1">
<circle
r="13"
cy="106.8622"
cx="138.57143"
id="circle4219"
style="opacity:1;fill:#cc0000;fill-opacity:1;stroke:#cc0000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#bbbbbb;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle4741"
cx="138.57143"
cy="72.362198"
r="13" />
<circle
r="13"
cy="37.862198"
cx="138.57143"
id="circle4743"
style="opacity:1;fill:none;fill-opacity:1;stroke:#bbbbbb;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="fill:#cc0000;fill-opacity:1"
id="path12-1"
d="m 67.62084,22.362177 c -15.906,0 -30.17278,10.945584 -29.67578,29.642574 l -0.0156,15.63282 c 0,0 0.23906,0.0215 -0.96094,0.0215 -1.27,0 -9.638671,1.47254 -9.638671,8.93554 l 0,35.824229 c 0,7.959 9.418671,9.94335 9.638671,9.94336 l 61.20313,0 c 0.22,0 9.15429,-0.99336 9.15429,-9.94336 l 0,-35.328129 c 10e-4,-8.455 -8.45469,-9.43164 -8.67969,-9.43164 l -1.92578,0 0,-15.6543 c -0.098,-19.194 -13.188609,-29.642578 -29.09961,-29.642578 z m -0.29297,11.947264 c 9.741001,0 15.904301,6.95735 15.904301,17.90235 l 0.01,15.44726 -31.835941,0 0.0156,-15.44726 c 0,-9.949 6.07125,-17.90235 15.90625,-17.90235 z"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccssscsscsccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -9,7 +9,7 @@ XXXDPI_DIR=$APP_DIR/res/drawable-xxxhdpi
SRC_DIR=./drawables-pgp/
for NAME in "status_lock" "status_lock_closed" "status_lock_error" "status_lock_open" "status_lock_disabled" "status_lock_opportunistic" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout"
for NAME in "bullet_point_positive" "bullet_point_negative" "compatibility" "status_lock" "status_lock_closed" "status_lock_error" "status_lock_open" "status_lock_disabled" "status_lock_opportunistic" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout"
do
echo $NAME
inkscape -w 24 -h 24 -e "$MDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"

View file

@ -57,4 +57,9 @@ public enum Flag {
* TODO Messages with this flag should be redownloaded, if possible.
*/
X_MIGRATED_FROM_V50,
/**
* This flag is used for drafts where the message should be sent as PGP/INLINE.
*/
X_DRAFT_OPENPGP_INLINE,
}

View file

@ -32,11 +32,16 @@ public class MimeMessageHelper {
setEncoding(part, MimeUtil.ENC_8BIT);
}
} else if (body instanceof TextBody) {
String contentType = String.format("%s;\r\n charset=utf-8", part.getMimeType());
String contentType;
if (MimeUtility.mimeTypeMatches(part.getMimeType(), "text/*")) {
contentType = String.format("%s;\r\n charset=utf-8", part.getMimeType());
String name = MimeUtility.getHeaderParameter(part.getContentType(), "name");
if (name != null) {
contentType += String.format(";\r\n name=\"%s\"", name);
}
} else {
contentType = part.getMimeType();
}
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
setEncoding(part, MimeUtil.ENC_8BIT);

View file

@ -132,4 +132,8 @@ public class TextBody implements Body, SizeAware {
return countingOutputStream.getCount();
}
public String getEncoding() {
return mEncoding;
}
}

View file

@ -30,6 +30,7 @@ dependencies {
compile 'com.github.bumptech.glide:glide:3.6.1'
compile 'com.splitwise:tokenautocomplete:2.0.7'
compile 'de.cketti.safecontentresolver:safe-content-resolver-v14:0.0.1'
compile 'com.github.amlcurran.showcaseview:library:5.4.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'

View file

@ -56,10 +56,12 @@ import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.compose.ComposeCryptoStatus;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState;
import com.fsck.k9.activity.compose.CryptoSettingsDialog.OnCryptoModeChangedListener;
import com.fsck.k9.activity.compose.IdentityAdapter;
import com.fsck.k9.activity.compose.IdentityAdapter.IdentityContainer;
import com.fsck.k9.activity.compose.PgpInlineDialog.OnOpenPgpInlineChangeListener;
import com.fsck.k9.activity.compose.RecipientMvpView;
import com.fsck.k9.activity.compose.RecipientPresenter;
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode;
@ -86,6 +88,7 @@ import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.LocalBodyPart;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.message.ComposePgpInlineDecider;
import com.fsck.k9.message.IdentityField;
import com.fsck.k9.message.IdentityHeaderParser;
import com.fsck.k9.message.MessageBuilder;
@ -101,7 +104,8 @@ import com.fsck.k9.ui.compose.QuotedMessagePresenter;
@SuppressWarnings("deprecation")
public class MessageCompose extends K9Activity implements OnClickListener,
CancelListener, OnFocusChangeListener, OnCryptoModeChangedListener, MessageBuilder.Callback {
CancelListener, OnFocusChangeListener, OnCryptoModeChangedListener,
OnOpenPgpInlineChangeListener, MessageBuilder.Callback {
private static final int DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE = 1;
private static final int DIALOG_CONFIRM_DISCARD_ON_BACK = 2;
@ -215,6 +219,11 @@ public class MessageCompose extends K9Activity implements OnClickListener,
recipientPresenter.onCryptoModeChanged(cryptoMode);
}
@Override
public void onOpenPgpInlineChange(boolean enabled) {
recipientPresenter.onCryptoPgpInlineChanged(enabled);
}
public enum Action {
COMPOSE(R.string.compose_title_compose),
REPLY(R.string.compose_title_reply),
@ -333,8 +342,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private FontSizes mFontSizes = K9.getFontSizes();
@Override
public void onCreate(Bundle savedInstanceState) {
@ -392,7 +399,8 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mChooseIdentityButton.setOnClickListener(this);
RecipientMvpView recipientMvpView = new RecipientMvpView(this);
recipientPresenter = new RecipientPresenter(this, recipientMvpView, mAccount);
ComposePgpInlineDecider composePgpInlineDecider = new ComposePgpInlineDecider();
recipientPresenter = new RecipientPresenter(this, recipientMvpView, mAccount, composePgpInlineDecider);
mSubjectView = (EditText) findViewById(R.id.subject);
mSubjectView.getInputExtras(true).putBoolean("allowEmoji", true);
@ -808,7 +816,8 @@ public class MessageCompose extends K9Activity implements OnClickListener,
.setSignatureChanged(mSignatureChanged)
.setCursorPosition(mMessageContentView.getSelectionStart())
.setMessageReference(mMessageReference)
.setDraft(isDraft);
.setDraft(isDraft)
.setIsPgpInlineEnabled(cryptoStatus.isPgpInlineModeEnabled());
quotedMessagePresenter.builderSetProperties(builder);
@ -928,6 +937,12 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
@SuppressLint("InlinedApi")
private void onAddAttachment() {
AttachErrorState maybeAttachErrorState = recipientPresenter.getCurrentCryptoStatus().getAttachErrorStateOrNull();
if (maybeAttachErrorState != null) {
recipientPresenter.showPgpAttachError(maybeAttachErrorState);
return;
}
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
i.addCategory(Intent.CATEGORY_OPENABLE);
@ -1286,6 +1301,12 @@ public class MessageCompose extends K9Activity implements OnClickListener,
case R.id.add_from_contacts:
recipientPresenter.onMenuAddFromContacts();
break;
case R.id.openpgp_inline_enable:
recipientPresenter.onMenuSetPgpInline(true);
break;
case R.id.openpgp_inline_disable:
recipientPresenter.onMenuSetPgpInline(false);
break;
case R.id.add_attachment:
onAddAttachment();
break;

View file

@ -25,6 +25,7 @@ public class ComposeCryptoStatus {
private Long signingKeyId;
private Long selfEncryptKeyId;
private String[] recipientAddresses;
private boolean enablePgpInline;
public long[] getEncryptKeyIds() {
@ -55,7 +56,7 @@ public class ComposeCryptoStatus {
// provider status is ok -> return value is based on cryptoMode
break;
default:
throw new AssertionError("all CryptoProviderStates must be handled, this is a bug!");
throw new AssertionError("all CryptoProviderStates must be handled!");
}
switch (cryptoMode) {
@ -82,7 +83,7 @@ public class ComposeCryptoStatus {
case DISABLE:
return CryptoStatusDisplayType.DISABLED;
default:
throw new AssertionError("all CryptoModes must be handled, this is a bug!");
throw new AssertionError("all CryptoModes must be handled!");
}
}
@ -102,6 +103,17 @@ public class ComposeCryptoStatus {
return cryptoMode != CryptoMode.DISABLE && signingKeyId != null;
}
public boolean isPgpInlineModeEnabled() {
return enablePgpInline;
}
public boolean isCryptoDisabled() {
return cryptoMode == CryptoMode.DISABLE;
}
public boolean isProviderStateOk() {
return cryptoProviderState == CryptoProviderState.OK;
}
public static class ComposeCryptoStatusBuilder {
@ -110,6 +122,7 @@ public class ComposeCryptoStatus {
private Long signingKeyId;
private Long selfEncryptKeyId;
private List<Recipient> recipients;
private Boolean enablePgpInline;
public ComposeCryptoStatusBuilder setCryptoProviderState(CryptoProviderState cryptoProviderState) {
this.cryptoProviderState = cryptoProviderState;
@ -136,15 +149,23 @@ public class ComposeCryptoStatus {
return this;
}
public ComposeCryptoStatusBuilder setEnablePgpInline(boolean cryptoEnableCompat) {
this.enablePgpInline = cryptoEnableCompat;
return this;
}
public ComposeCryptoStatus build() {
if (cryptoProviderState == null) {
throw new AssertionError("cryptoProviderState must be set. this is a bug!");
throw new AssertionError("cryptoProviderState must be set!");
}
if (cryptoMode == null) {
throw new AssertionError("crypto mode must be set. this is a bug!");
throw new AssertionError("crypto mode must be set!");
}
if (recipients == null) {
throw new AssertionError("recipients must be set. this is a bug!");
throw new AssertionError("recipients must be set!");
}
if (enablePgpInline == null) {
throw new AssertionError("enablePgpInline must be set!");
}
ArrayList<String> recipientAddresses = new ArrayList<>();
@ -172,6 +193,7 @@ public class ComposeCryptoStatus {
result.hasRecipients = hasRecipients;
result.signingKeyId = signingKeyId;
result.selfEncryptKeyId = selfEncryptKeyId;
result.enablePgpInline = enablePgpInline;
return result;
}
}
@ -197,4 +219,20 @@ public class ComposeCryptoStatus {
return null;
}
public enum AttachErrorState {
IS_INLINE
}
public AttachErrorState getAttachErrorStateOrNull() {
if (cryptoProviderState == CryptoProviderState.UNCONFIGURED) {
return null;
}
if (enablePgpInline) {
return AttachErrorState.IS_INLINE;
}
return null;
}
}

View file

@ -0,0 +1,79 @@
package com.fsck.k9.activity.compose;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.view.LayoutInflater;
import android.view.View;
import com.fsck.k9.R;
import com.fsck.k9.view.HighlightDialogFragment;
public class PgpInlineDialog extends HighlightDialogFragment {
public static final String ARG_FIRST_TIME = "first_time";
public static PgpInlineDialog newInstance(boolean firstTime, @IdRes int showcaseView) {
PgpInlineDialog dialog = new PgpInlineDialog();
Bundle args = new Bundle();
args.putInt(ARG_FIRST_TIME, firstTime ? 1 : 0);
args.putInt(ARG_HIGHLIGHT_VIEW, showcaseView);
dialog.setArguments(args);
return dialog;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Activity activity = getActivity();
@SuppressLint("InflateParams")
View view = LayoutInflater.from(activity).inflate(R.layout.openpgp_inline_dialog, null);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(view);
if (getArguments().getInt(ARG_FIRST_TIME) != 0) {
builder.setPositiveButton(R.string.openpgp_inline_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
} else {
builder.setPositiveButton(R.string.openpgp_inline_disable, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Activity activity = getActivity();
if (activity == null) {
return;
}
((OnOpenPgpInlineChangeListener) activity).onOpenPgpInlineChange(false);
dialog.dismiss();
}
});
builder.setNegativeButton(R.string.openpgp_inline_keep_enabled, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
}
return builder.create();
}
public interface OnOpenPgpInlineChangeListener {
void onOpenPgpInlineChange(boolean enabled);
}
}

View file

@ -47,6 +47,7 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
private final RecipientSelectView bccView;
private final ViewAnimator cryptoStatusView;
private final ViewAnimator recipientExpanderContainer;
private final View pgpInlineIndicator;
private RecipientPresenter presenter;
@ -63,6 +64,7 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
recipientExpanderContainer = (ViewAnimator) activity.findViewById(R.id.recipient_expander_container);
cryptoStatusView = (ViewAnimator) activity.findViewById(R.id.crypto_status);
cryptoStatusView.setOnClickListener(this);
pgpInlineIndicator = activity.findViewById(R.id.pgp_inline_indicator);
toView.setOnFocusChangeListener(this);
ccView.setOnFocusChangeListener(this);
@ -77,6 +79,8 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
toLabel.setOnClickListener(this);
ccLabel.setOnClickListener(this);
bccLabel.setOnClickListener(this);
pgpInlineIndicator.setOnClickListener(this);
}
public void setPresenter(final RecipientPresenter presenter) {
@ -265,6 +269,11 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
bccView.setError(bccView.getContext().getString(R.string.compose_error_incomplete_recipient));
}
public void showPgpInlineModeIndicator(boolean pgpInlineModeEnabled) {
pgpInlineIndicator.setVisibility(pgpInlineModeEnabled ? View.VISIBLE : View.GONE);
activity.invalidateOptionsMenu();
}
public void showCryptoStatus(final CryptoStatusDisplayType cryptoStatusDisplayType) {
boolean shouldBeHidden = cryptoStatusDisplayType.childToDisplay == VIEW_INDEX_HIDDEN;
if (shouldBeHidden) {
@ -301,6 +310,10 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
Toast.makeText(activity, R.string.compose_error_private_missing_keys, Toast.LENGTH_LONG).show();
}
public void showErrorAttachInline() {
Toast.makeText(activity, R.string.error_crypto_inline_attach, Toast.LENGTH_LONG).show();
}
@Override
public void onFocusChange(View view, boolean hasFocus) {
if (!hasFocus) {
@ -346,6 +359,9 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
presenter.onClickCryptoStatus();
break;
}
case R.id.pgp_inline_indicator: {
presenter.onClickPgpInlineIndicator();
}
}
}
@ -354,6 +370,11 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
dialog.show(activity.getFragmentManager(), "crypto_settings");
}
public void showOpenPgpInlineDialog(boolean firstTime) {
PgpInlineDialog dialog = PgpInlineDialog.newInstance(firstTime, R.id.pgp_inline_indicator);
dialog.show(activity.getFragmentManager(), "openpgp_inline");
}
public void launchUserInteractionPendingIntent(PendingIntent pendingIntent, int requestCode) {
activity.launchUserInteractionPendingIntent(pendingIntent, requestCode);
}

View file

@ -21,6 +21,7 @@ import com.fsck.k9.Account;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.ComposeCryptoStatusBuilder;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState;
import com.fsck.k9.helper.Contacts;
@ -28,11 +29,13 @@ import com.fsck.k9.helper.MailTo;
import com.fsck.k9.helper.ReplyToParser;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.message.PgpMessageBuilder;
import com.fsck.k9.message.ComposePgpInlineDecider;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi;
@ -45,7 +48,8 @@ public class RecipientPresenter implements PermissionPingCallback {
private static final String STATE_KEY_CC_SHOWN = "state:ccShown";
private static final String STATE_KEY_BCC_SHOWN = "state:bccShown";
private static final String STATE_KEY_LAST_FOCUSED_TYPE = "state:lastFocusedType";
private static final String STATE_KEY_CURRENT_CRYPTO_MODE = "key:initialOrFormerCryptoMode";
private static final String STATE_KEY_CURRENT_CRYPTO_MODE = "state:currentCryptoMode";
private static final String STATE_KEY_CRYPTO_ENABLE_PGP_INLINE = "state:cryptoEnablePgpInline";
private static final int CONTACT_PICKER_TO = 1;
private static final int CONTACT_PICKER_CC = 2;
@ -56,6 +60,7 @@ public class RecipientPresenter implements PermissionPingCallback {
// transient state, which is either obtained during construction and initialization, or cached
private final Context context;
private final RecipientMvpView recipientMvpView;
private final ComposePgpInlineDecider composePgpInlineDecider;
private Account account;
private String cryptoProvider;
private Boolean hasContactPicker;
@ -69,11 +74,14 @@ public class RecipientPresenter implements PermissionPingCallback {
private RecipientType lastFocusedType = RecipientType.TO;
// TODO initialize cryptoMode to other values under some circumstances, e.g. if we reply to an encrypted e-mail
private CryptoMode currentCryptoMode = CryptoMode.OPPORTUNISTIC;
private boolean cryptoEnablePgpInline = false;
public RecipientPresenter(Context context, RecipientMvpView recipientMvpView, Account account) {
public RecipientPresenter(Context context, RecipientMvpView recipientMvpView, Account account,
ComposePgpInlineDecider composePgpInlineDecider) {
this.recipientMvpView = recipientMvpView;
this.context = context;
this.composePgpInlineDecider = composePgpInlineDecider;
recipientMvpView.setPresenter(this);
onSwitchAccount(account);
@ -161,6 +169,12 @@ public class RecipientPresenter implements PermissionPingCallback {
}
}
boolean shouldSendAsPgpInline = composePgpInlineDecider.shouldReplyInline(message);
if (shouldSendAsPgpInline) {
cryptoEnablePgpInline = true;
}
} catch (MessagingException e) {
// can't happen, we know the recipient types exist
throw new AssertionError(e);
@ -196,6 +210,7 @@ public class RecipientPresenter implements PermissionPingCallback {
recipientMvpView.setBccVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN));
lastFocusedType = RecipientType.valueOf(savedInstanceState.getString(STATE_KEY_LAST_FOCUSED_TYPE));
currentCryptoMode = CryptoMode.valueOf(savedInstanceState.getString(STATE_KEY_CURRENT_CRYPTO_MODE));
cryptoEnablePgpInline = savedInstanceState.getBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE);
updateRecipientExpanderVisibility();
}
@ -204,9 +219,15 @@ public class RecipientPresenter implements PermissionPingCallback {
outState.putBoolean(STATE_KEY_BCC_SHOWN, recipientMvpView.isBccVisible());
outState.putString(STATE_KEY_LAST_FOCUSED_TYPE, lastFocusedType.toString());
outState.putString(STATE_KEY_CURRENT_CRYPTO_MODE, currentCryptoMode.toString());
outState.putBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE, cryptoEnablePgpInline);
}
public void initFromDraftMessage(LocalMessage message) {
initRecipientsFromDraftMessage(message);
initPgpInlineFromDraftMessage(message);
}
private void initRecipientsFromDraftMessage(LocalMessage message) {
try {
addToAddresses(message.getRecipients(RecipientType.TO));
@ -221,6 +242,10 @@ public class RecipientPresenter implements PermissionPingCallback {
}
}
private void initPgpInlineFromDraftMessage(LocalMessage message) {
cryptoEnablePgpInline = message.isSet(Flag.X_DRAFT_OPENPGP_INLINE);
}
void addToAddresses(Address... toAddresses) {
addRecipientsFromAddresses(RecipientType.TO, toAddresses);
}
@ -248,6 +273,10 @@ public class RecipientPresenter implements PermissionPingCallback {
}
public void onPrepareOptionsMenu(Menu menu) {
boolean isCryptoConfigured = cryptoProviderState != CryptoProviderState.UNCONFIGURED;
menu.findItem(R.id.openpgp_inline_enable).setVisible(isCryptoConfigured && !cryptoEnablePgpInline);
menu.findItem(R.id.openpgp_inline_disable).setVisible(isCryptoConfigured && cryptoEnablePgpInline);
boolean noContactPickerAvailable = !hasContactPicker();
if (noContactPickerAvailable) {
menu.findItem(R.id.add_from_contacts).setVisible(false);
@ -345,6 +374,7 @@ public class RecipientPresenter implements PermissionPingCallback {
}
recipientMvpView.showCryptoStatus(getCurrentCryptoStatus().getCryptoStatusDisplayType());
recipientMvpView.showPgpInlineModeIndicator(getCurrentCryptoStatus().isPgpInlineModeEnabled());
}
public ComposeCryptoStatus getCurrentCryptoStatus() {
@ -352,6 +382,7 @@ public class RecipientPresenter implements PermissionPingCallback {
ComposeCryptoStatusBuilder builder = new ComposeCryptoStatusBuilder()
.setCryptoProviderState(cryptoProviderState)
.setCryptoMode(currentCryptoMode)
.setEnablePgpInline(cryptoEnablePgpInline)
.setRecipients(getAllRecipients());
long accountCryptoKey = account.getCryptoKey();
@ -427,6 +458,11 @@ public class RecipientPresenter implements PermissionPingCallback {
updateCryptoStatus();
}
public void onCryptoPgpInlineChanged(boolean enablePgpInline) {
cryptoEnablePgpInline = enablePgpInline;
updateCryptoStatus();
}
private void addRecipientsFromAddresses(final RecipientType recipientType, final Address... addresses) {
new RecipientLoader(context, cryptoProvider, addresses) {
@Override
@ -578,6 +614,16 @@ public class RecipientPresenter implements PermissionPingCallback {
}
}
public void showPgpAttachError(AttachErrorState attachErrorState) {
switch (attachErrorState) {
case IS_INLINE:
recipientMvpView.showErrorAttachInline();
break;
default:
throw new AssertionError("not all error states handled, this is a bug!");
}
}
private void setCryptoProvider(String cryptoProvider) {
boolean providerIsBound = openPgpServiceConnection != null && openPgpServiceConnection.isBound();
@ -683,11 +729,24 @@ public class RecipientPresenter implements PermissionPingCallback {
return new OpenPgpApi(context, openPgpServiceConnection.getService());
}
public void builderSetProperties(PgpMessageBuilder pgpBuilder) {
pgpBuilder.setOpenPgpApi(getOpenPgpApi());
pgpBuilder.setCryptoStatus(getCurrentCryptoStatus());
}
public void onMenuSetPgpInline(boolean enablePgpInline) {
cryptoEnablePgpInline = enablePgpInline;
updateCryptoStatus();
if (enablePgpInline) {
recipientMvpView.showOpenPgpInlineDialog(true);
}
}
public void onClickPgpInlineIndicator() {
recipientMvpView.showOpenPgpInlineDialog(false);
}
public enum CryptoProviderState {
UNCONFIGURED,
UNINITIALIZED,

View file

@ -0,0 +1,21 @@
package com.fsck.k9.message;
import java.util.List;
import com.fsck.k9.crypto.MessageDecryptVerifier;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
public class ComposePgpInlineDecider {
public boolean shouldReplyInline(Message localMessage) {
// TODO more criteria for this? maybe check the User-Agent header?
return messageHasPgpInlineParts(localMessage);
}
private boolean messageHasPgpInlineParts(Message localMessage) {
List<Part> inlineParts = MessageDecryptVerifier.findPgpInlineParts(localMessage);
return !inlineParts.isEmpty();
}
}

View file

@ -20,6 +20,7 @@ import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.activity.misc.Attachment;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeBodyPart;
@ -61,6 +62,7 @@ public abstract class MessageBuilder {
private int cursorPosition;
private MessageReference messageReference;
private boolean isDraft;
private boolean isPgpInlineEnabled;
public MessageBuilder(Context context) {
this.context = context;
@ -70,7 +72,7 @@ public abstract class MessageBuilder {
* Build the message to be sent (or saved). If there is another message quoted in this one, it will be baked
* into the message here.
*/
public MimeMessage build() throws MessagingException {
protected MimeMessage build() throws MessagingException {
//FIXME: check arguments
MimeMessage message = new MimeMessage();
@ -114,6 +116,10 @@ public abstract class MessageBuilder {
}
message.generateMessageId();
if (isDraft && isPgpInlineEnabled) {
message.setFlag(Flag.X_DRAFT_OPENPGP_INLINE, true);
}
}
private void buildBody(MimeMessage message) throws MessagingException {
@ -435,6 +441,11 @@ public abstract class MessageBuilder {
return this;
}
public MessageBuilder setIsPgpInlineEnabled(boolean isPgpInlineEnabled) {
this.isPgpInlineEnabled = isPgpInlineEnabled;
return this;
}
public boolean isDraft() {
return isDraft;
}

View file

@ -2,11 +2,13 @@ package com.fsck.k9.message;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
@ -21,8 +23,10 @@ import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMessageHelper;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.BinaryMemoryBody;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.util.MimeUtil;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpApi;
@ -31,13 +35,14 @@ import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource;
public class PgpMessageBuilder extends MessageBuilder {
public static final int REQUEST_SIGN_INTERACTION = 1;
public static final int REQUEST_ENCRYPT_INTERACTION = 2;
public static final int REQUEST_USER_INTERACTION = 1;
private OpenPgpApi openPgpApi;
private MimeMessage currentProcessedMimeMessage;
private ComposeCryptoStatus cryptoStatus;
private boolean opportunisticSkipEncryption;
private boolean opportunisticSecondPass;
public PgpMessageBuilder(Context context) {
super(context);
@ -47,73 +52,70 @@ public class PgpMessageBuilder extends MessageBuilder {
this.openPgpApi = openPgpApi;
}
/** This class keeps track of its internal state explicitly. */
private enum State {
IDLE, START, FAILURE,
OPENPGP_SIGN, OPENPGP_SIGN_UI, OPENPGP_SIGN_OK,
OPENPGP_ENCRYPT, OPENPGP_ENCRYPT_UI, OPENPGP_ENCRYPT_OK;
public boolean isBreakState() {
return this == OPENPGP_SIGN_UI || this == OPENPGP_ENCRYPT_UI || this == FAILURE;
}
public boolean isReentrantState() {
return this == OPENPGP_SIGN || this == OPENPGP_ENCRYPT;
}
public boolean isSignOk() {
return this == OPENPGP_SIGN_OK || this == OPENPGP_ENCRYPT
|| this == OPENPGP_ENCRYPT_UI || this == OPENPGP_ENCRYPT_OK;
}
}
State currentState = State.IDLE;
@Override
protected void buildMessageInternal() {
if (currentState != State.IDLE) {
throw new IllegalStateException("internal error, a PgpMessageBuilder can only be built once!");
if (currentProcessedMimeMessage != null) {
throw new IllegalStateException("message can only be built once!");
}
if (cryptoStatus == null) {
throw new IllegalStateException("PgpMessageBuilder must have cryptoStatus set before building!");
}
if (cryptoStatus.isCryptoDisabled()) {
throw new AssertionError("PgpMessageBuilder must not be used if crypto is disabled!");
}
try {
if (!cryptoStatus.isProviderStateOk()) {
throw new MessagingException("OpenPGP Provider is not ready!");
}
currentProcessedMimeMessage = build();
} catch (MessagingException me) {
queueMessageBuildException(me);
return;
}
currentState = State.START;
startOrContinueBuildMessage(null);
}
@Override
public void buildMessageOnActivityResult(int requestCode, Intent userInteractionResult) {
if (requestCode == REQUEST_SIGN_INTERACTION && currentState == State.OPENPGP_SIGN_UI) {
currentState = State.OPENPGP_SIGN;
startOrContinueBuildMessage(userInteractionResult);
} else if (requestCode == REQUEST_ENCRYPT_INTERACTION && currentState == State.OPENPGP_ENCRYPT_UI) {
currentState = State.OPENPGP_ENCRYPT;
startOrContinueBuildMessage(userInteractionResult);
} else {
throw new IllegalStateException("illegal state!");
public void buildMessageOnActivityResult(int requestCode, @NonNull Intent userInteractionResult) {
if (currentProcessedMimeMessage == null) {
throw new AssertionError("build message from activity result must not be called individually");
}
startOrContinueBuildMessage(userInteractionResult);
}
private void startOrContinueBuildMessage(@Nullable Intent userInteractionResult) {
if (currentState != State.START && !currentState.isReentrantState()) {
throw new IllegalStateException("bad state!");
}
private void startOrContinueBuildMessage(@Nullable Intent pgpApiIntent) {
try {
startOrContinueSigningIfRequested(userInteractionResult);
boolean shouldSign = cryptoStatus.isSigningEnabled();
boolean shouldEncrypt = cryptoStatus.isEncryptionEnabled() && !opportunisticSkipEncryption;
boolean isPgpInlineMode = cryptoStatus.isPgpInlineModeEnabled();
if (currentState.isBreakState()) {
if (!shouldSign && !shouldEncrypt) {
return;
}
startOrContinueEncryptionIfRequested(userInteractionResult);
boolean isSimpleTextMessage =
MimeUtility.isSameMimeType("text/plain", currentProcessedMimeMessage.getMimeType());
if (isPgpInlineMode && !isSimpleTextMessage) {
throw new MessagingException("Attachments are not supported in PGP/INLINE format!");
}
if (currentState.isBreakState()) {
if (pgpApiIntent == null) {
pgpApiIntent = buildOpenPgpApiIntent(shouldSign, shouldEncrypt, isPgpInlineMode);
}
PendingIntent returnedPendingIntent = launchOpenPgpApiIntent(
pgpApiIntent, shouldEncrypt || isPgpInlineMode, shouldEncrypt || !isPgpInlineMode, isPgpInlineMode);
if (returnedPendingIntent != null) {
queueMessageBuildPendingIntent(returnedPendingIntent, REQUEST_USER_INTERACTION);
return;
}
if (opportunisticSkipEncryption && !opportunisticSecondPass) {
opportunisticSecondPass = true;
startOrContinueBuildMessage(null);
return;
}
@ -123,97 +125,63 @@ public class PgpMessageBuilder extends MessageBuilder {
}
}
private void startOrContinueEncryptionIfRequested(Intent userInteractionResult) throws MessagingException {
boolean reenterOperation = currentState == State.OPENPGP_ENCRYPT;
if (reenterOperation) {
mimeIntentLaunch(userInteractionResult);
return;
@NonNull
private Intent buildOpenPgpApiIntent(boolean shouldSign, boolean shouldEncrypt, boolean isPgpInlineMode)
throws MessagingException {
Intent pgpApiIntent;
if (shouldEncrypt) {
if (!shouldSign) {
throw new IllegalStateException("encrypt-only is not supported at this point and should never happen!");
}
if (!cryptoStatus.isEncryptionEnabled()) {
return;
}
Intent encryptIntent = new Intent(OpenPgpApi.ACTION_ENCRYPT);
encryptIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
// pgpApiIntent = new Intent(shouldSign ? OpenPgpApi.ACTION_SIGN_AND_ENCRYPT : OpenPgpApi.ACTION_ENCRYPT);
pgpApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
long[] encryptKeyIds = cryptoStatus.getEncryptKeyIds();
if (encryptKeyIds != null) {
encryptIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, encryptKeyIds);
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, encryptKeyIds);
}
if(!isDraft()) {
String[] encryptRecipientAddresses = cryptoStatus.getRecipientAddresses();
boolean hasRecipientAddresses = encryptRecipientAddresses != null && encryptRecipientAddresses.length > 0;
if (!hasRecipientAddresses) {
// TODO safeguard here once this is better handled by the caller?
// throw new MessagingException("Encryption is enabled, but no encryption key specified!");
return;
throw new MessagingException("encryption is enabled, but no recipient specified!");
}
encryptIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, encryptRecipientAddresses);
encryptIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic());
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, encryptRecipientAddresses);
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic());
}
} else {
pgpApiIntent = new Intent(isPgpInlineMode ? OpenPgpApi.ACTION_SIGN : OpenPgpApi.ACTION_DETACHED_SIGN);
}
currentState = State.OPENPGP_ENCRYPT;
mimeIntentLaunch(encryptIntent);
if (shouldSign) {
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, cryptoStatus.getSigningKeyId());
}
private void startOrContinueSigningIfRequested(Intent userInteractionResult) throws MessagingException {
boolean reenterOperation = currentState == State.OPENPGP_SIGN;
if (reenterOperation) {
mimeIntentLaunch(userInteractionResult);
return;
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
return pgpApiIntent;
}
boolean signingDisabled = !cryptoStatus.isSigningEnabled();
boolean alreadySigned = currentState.isSignOk();
boolean isDraft = isDraft();
if (signingDisabled || alreadySigned || isDraft) {
return;
}
Intent signIntent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN);
signIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, cryptoStatus.getSigningKeyId());
currentState = State.OPENPGP_SIGN;
mimeIntentLaunch(signIntent);
}
/** This method executes the given Intent with the OpenPGP Api. It will pass the
* entire current message as input. On success, either mimeBuildSignedMessage() or
* mimeBuildEncryptedMessage() will be called with their appropriate inputs. If an
* error or PendingInput is returned, this will be passed as a result to the
* operation.
*/
private void mimeIntentLaunch(Intent openPgpIntent) throws MessagingException {
private PendingIntent launchOpenPgpApiIntent(@NonNull Intent openPgpIntent,
boolean captureOutputPart, boolean capturedOutputPartIs7Bit, boolean writeBodyContentOnly) throws MessagingException {
final MimeBodyPart bodyPart = currentProcessedMimeMessage.toBodyPart();
String[] contentType = currentProcessedMimeMessage.getHeader(MimeHeader.HEADER_CONTENT_TYPE);
if (contentType.length > 0) {
bodyPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType[0]);
}
bodyPart.setUsing7bitTransport();
// This data will be read in a worker thread
OpenPgpDataSource dataSource = new OpenPgpDataSource() {
@Override
public void writeTo(OutputStream os) throws IOException {
try {
bodyPart.writeTo(os);
} catch (MessagingException e) {
throw new IOException(e);
}
}
};
OpenPgpDataSource dataSource = createOpenPgpDataSourceFromBodyPart(bodyPart, writeBodyContentOnly);
BinaryTempFileBody encryptedTempBody = null;
BinaryTempFileBody pgpResultTempBody = null;
OutputStream outputStream = null;
if (currentState == State.OPENPGP_ENCRYPT) {
if (captureOutputPart) {
try {
encryptedTempBody = new BinaryTempFileBody(MimeUtil.ENC_7BIT);
outputStream = encryptedTempBody.getOutputStream();
pgpResultTempBody = new BinaryTempFileBody(
capturedOutputPartIs7Bit ? MimeUtil.ENC_7BIT : MimeUtil.ENC_8BIT);
outputStream = pgpResultTempBody.getOutputStream();
} catch (IOException e) {
throw new MessagingException("Could not allocate temp file for storage!", e);
throw new MessagingException("could not allocate temp file for storage!", e);
}
}
@ -221,56 +189,91 @@ public class PgpMessageBuilder extends MessageBuilder {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
if (currentState == State.OPENPGP_SIGN) {
mimeBuildSignedMessage(bodyPart, result);
} else if (currentState == State.OPENPGP_ENCRYPT) {
mimeBuildEncryptedMessage(encryptedTempBody, result);
} else {
throw new IllegalStateException("state error!");
}
return;
mimeBuildMessage(result, bodyPart, pgpResultTempBody);
return null;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
launchUserInteraction(result);
return;
PendingIntent returnedPendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
if (returnedPendingIntent == null) {
throw new MessagingException("openpgp api needs user interaction, but returned no pendingintent!");
}
return returnedPendingIntent;
case OpenPgpApi.RESULT_CODE_ERROR:
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
if (error == null) {
throw new MessagingException("internal openpgp api error");
}
boolean isOpportunisticError = error.getErrorId() == OpenPgpError.OPPORTUNISTIC_MISSING_KEYS;
if (isOpportunisticError) {
skipEncryptingMessage();
return;
return null;
}
throw new MessagingException(error.getMessage());
default:
throw new IllegalStateException("unreachable code segment reached - this is a bug");
}
}
private void launchUserInteraction(Intent result) {
PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
throw new IllegalStateException("unreachable code segment reached");
}
if (currentState == State.OPENPGP_ENCRYPT) {
currentState = State.OPENPGP_ENCRYPT_UI;
queueMessageBuildPendingIntent(pendingIntent, REQUEST_ENCRYPT_INTERACTION);
} else if (currentState == State.OPENPGP_SIGN) {
currentState = State.OPENPGP_SIGN_UI;
queueMessageBuildPendingIntent(pendingIntent, REQUEST_SIGN_INTERACTION);
@NonNull
private OpenPgpDataSource createOpenPgpDataSourceFromBodyPart(final MimeBodyPart bodyPart,
final boolean writeBodyContentOnly)
throws MessagingException {
return new OpenPgpDataSource() {
@Override
public void writeTo(OutputStream os) throws IOException {
try {
if (writeBodyContentOnly) {
Body body = bodyPart.getBody();
InputStream inputStream = body.getInputStream();
IOUtils.copy(inputStream, os);
} else {
throw new IllegalStateException("illegal state!");
bodyPart.writeTo(os);
}
} catch (MessagingException e) {
throw new IOException(e);
}
}
};
}
private void mimeBuildMessage(
@NonNull Intent result, @NonNull MimeBodyPart bodyPart, @Nullable BinaryTempFileBody pgpResultTempBody)
throws MessagingException {
if (pgpResultTempBody == null) {
boolean shouldHaveResultPart = cryptoStatus.isPgpInlineModeEnabled() ||
(cryptoStatus.isEncryptionEnabled() && !opportunisticSkipEncryption);
if (shouldHaveResultPart) {
throw new AssertionError("encryption or pgp/inline is enabled, but no output part!");
}
mimeBuildSignedMessage(bodyPart, result);
return;
}
if (cryptoStatus.isPgpInlineModeEnabled()) {
mimeBuildInlineMessage(pgpResultTempBody);
return;
}
mimeBuildEncryptedMessage(pgpResultTempBody);
}
private void mimeBuildSignedMessage(@NonNull BodyPart signedBodyPart, Intent result) throws MessagingException {
if (!cryptoStatus.isSigningEnabled()) {
throw new IllegalStateException("call to mimeBuildSignedMessage while signing isn't enabled!");
}
private void mimeBuildSignedMessage(BodyPart signedBodyPart, Intent result) throws MessagingException {
byte[] signedData = result.getByteArrayExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE);
if (signedData == null) {
throw new MessagingException("didn't find expected RESULT_DETACHED_SIGNATURE in api call result");
}
MimeMultipart multipartSigned = new MimeMultipart();
multipartSigned.setSubType("signed");
multipartSigned.addBodyPart(signedBodyPart);
multipartSigned.addBodyPart(
new MimeBodyPart(new BinaryMemoryBody(signedData, MimeUtil.ENC_7BIT), "application/pgp-signature"));
MimeMessageHelper.setBody(currentProcessedMimeMessage, multipartSigned);
String contentType = String.format(
@ -283,113 +286,46 @@ public class PgpMessageBuilder extends MessageBuilder {
Log.e(K9.LOG_TAG, "missing micalg parameter for pgp multipart/signed!");
}
currentProcessedMimeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
currentState = State.OPENPGP_SIGN_OK;
}
@SuppressWarnings("UnusedParameters")
private void mimeBuildEncryptedMessage(Body encryptedBodyPart, Intent result) throws MessagingException {
private void mimeBuildEncryptedMessage(@NonNull Body encryptedBodyPart) throws MessagingException {
if (!cryptoStatus.isEncryptionEnabled()) {
throw new IllegalStateException("call to mimeBuildEncryptedMessage while encryption isn't enabled!");
}
MimeMultipart multipartEncrypted = new MimeMultipart();
multipartEncrypted.setSubType("encrypted");
multipartEncrypted.addBodyPart(new MimeBodyPart(new TextBody("Version: 1"), "application/pgp-encrypted"));
multipartEncrypted.addBodyPart(new MimeBodyPart(encryptedBodyPart, "application/octet-stream"));
MimeMessageHelper.setBody(currentProcessedMimeMessage, multipartEncrypted);
String contentType = String.format(
"multipart/encrypted; boundary=\"%s\";\r\n protocol=\"application/pgp-encrypted\"",
multipartEncrypted.getBoundary());
currentProcessedMimeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
}
currentState = State.OPENPGP_ENCRYPT_OK;
private void mimeBuildInlineMessage(@NonNull Body inlineBodyPart) throws MessagingException {
if (!cryptoStatus.isPgpInlineModeEnabled()) {
throw new IllegalStateException("call to mimeBuildInlineMessage while pgp/inline isn't enabled!");
}
boolean isCleartextSignature = !cryptoStatus.isEncryptionEnabled();
if (isCleartextSignature) {
inlineBodyPart.setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
}
MimeMessageHelper.setBody(currentProcessedMimeMessage, inlineBodyPart);
}
private void skipEncryptingMessage() throws MessagingException {
if (!cryptoStatus.isEncryptionOpportunistic()) {
throw new AssertionError("Got opportunistic error, but encryption wasn't supposed to be opportunistic!");
}
currentState = State.OPENPGP_ENCRYPT_OK;
opportunisticSkipEncryption = true;
}
public void setCryptoStatus(ComposeCryptoStatus cryptoStatus) {
this.cryptoStatus = cryptoStatus;
}
/* TODO re-add PGP/INLINE
if (isCryptoProviderEnabled() && ! mAccount.isUsePgpMime()) {
// OpenPGP Provider API
// If not already encrypted but user wants to encrypt...
if (mPgpData.getEncryptedData() == null &&
(mEncryptCheckbox.isChecked() || mCryptoSignatureCheckbox.isChecked())) {
String[] emailsArray = null;
if (mEncryptCheckbox.isChecked()) {
// get emails as array
List<String> emails = new ArrayList<String>();
for (Address address : recipientPresenter.getAllRecipientAddresses()) {
emails.add(address.getAddress());
}
emailsArray = emails.toArray(new String[emails.size()]);
}
if (mEncryptCheckbox.isChecked() && mCryptoSignatureCheckbox.isChecked()) {
Intent intent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, emailsArray);
intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, mAccount.getCryptoKey());
executeOpenPgpMethod(intent);
} else if (mCryptoSignatureCheckbox.isChecked()) {
Intent intent = new Intent(OpenPgpApi.ACTION_SIGN);
intent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, mAccount.getCryptoKey());
executeOpenPgpMethod(intent);
} else if (mEncryptCheckbox.isChecked()) {
Intent intent = new Intent(OpenPgpApi.ACTION_ENCRYPT);
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, emailsArray);
executeOpenPgpMethod(intent);
}
// onSend() is called again in SignEncryptCallback and with
// encryptedData set in pgpData!
return;
}
}
*/
/* TODO re-add attach public key
private Attachment attachedPublicKey() throws OpenPgpApiException {
try {
Attachment publicKey = new Attachment();
publicKey.contentType = "application/pgp-keys";
String keyName = "0x" + Long.toString(mAccount.getCryptoKey(), 16).substring(8);
publicKey.name = keyName + ".asc";
Intent intent = new Intent(OpenPgpApi.ACTION_GET_KEY);
intent.putExtra(OpenPgpApi.EXTRA_KEY_ID, mAccount.getCryptoKey());
intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
OpenPgpApi api = new OpenPgpApi(this, mOpenPgpServiceConnection.getService());
File keyTempFile = File.createTempFile("key", ".asc", getCacheDir());
keyTempFile.deleteOnExit();
try {
CountingOutputStream keyFileStream = new CountingOutputStream(new BufferedOutputStream(
new FileOutputStream(keyTempFile)));
Intent res = api.executeApi(intent, null, new EOLConvertingOutputStream(keyFileStream));
if (res.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) != OpenPgpApi.RESULT_CODE_SUCCESS
|| keyFileStream.getByteCount() == 0) {
keyTempFile.delete();
throw new OpenPgpApiException(String.format(getString(R.string.openpgp_no_public_key_returned),
getString(R.string.btn_attach_key)));
}
publicKey.filename = keyTempFile.getAbsolutePath();
publicKey.state = Attachment.LoadingState.COMPLETE;
publicKey.size = keyFileStream.getByteCount();
return publicKey;
} catch(RuntimeException e){
keyTempFile.delete();
throw e;
}
} catch(IOException e){
throw new RuntimeException(getString(R.string.error_cant_create_temporary_file), e);
}
}
*/
}

View file

@ -1,13 +1,15 @@
package com.fsck.k9.message;
import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Body;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.internet.TextBody;
class TextBodyBuilder {
private boolean mIncludeQuotedText = true;
private boolean mReplyAfterQuote = false;

View file

@ -0,0 +1,98 @@
package com.fsck.k9.view;
import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import com.fsck.k9.R;
import com.github.amlcurran.showcaseview.ShowcaseView;
import com.github.amlcurran.showcaseview.ShowcaseView.Builder;
import com.github.amlcurran.showcaseview.targets.ViewTarget;
public class HighlightDialogFragment extends DialogFragment {
public static final String ARG_HIGHLIGHT_VIEW = "highlighted_view";
public static final float BACKGROUND_DIM_AMOUNT = 0.25f;
private ShowcaseView showcaseView;
protected void highlightViewInBackground() {
if (!getArguments().containsKey(ARG_HIGHLIGHT_VIEW)) {
return;
}
Activity activity = getActivity();
if (activity == null) {
throw new IllegalStateException("fragment must be attached to set highlight!");
}
boolean alreadyShowing = showcaseView != null && showcaseView.isShowing();
if (alreadyShowing) {
return;
}
int highlightedView = getArguments().getInt(ARG_HIGHLIGHT_VIEW);
showcaseView = new Builder(activity)
.setTarget(new ViewTarget(highlightedView, activity))
.hideOnTouchOutside()
.blockAllTouches()
.withMaterialShowcase()
.setStyle(R.style.ShowcaseTheme)
.build();
showcaseView.hideButton();
}
@Override
public void onStart() {
super.onStart();
hideKeyboard();
highlightViewInBackground();
setDialogBackgroundDim();
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
hideShowcaseView();
}
private void setDialogBackgroundDim() {
Dialog dialog = getDialog();
if (dialog == null) {
return;
}
dialog.getWindow().setDimAmount(BACKGROUND_DIM_AMOUNT);
}
private void hideKeyboard() {
Activity activity = getActivity();
if (activity == null) {
return;
}
// check if no view has focus
View v = activity.getCurrentFocus();
if (v == null) {
return;
}
InputMethodManager inputManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
private void hideShowcaseView() {
if (showcaseView != null && showcaseView.isShowing()) {
showcaseView.hide();
}
showcaseView = null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -10,54 +10,66 @@
android:orientation="vertical"
tools:showIn="@layout/message_compose">
<RelativeLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginLeft="12dip"
android:layout_marginStart="12dip"
android:layout_marginRight="10dip"
android:layout_marginEnd="10dip"
android:minHeight="50dp">
android:minHeight="50dp"
android:animateLayoutChanges="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_gravity="center_vertical"
android:id="@+id/from_label"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:id="@+id/from_label"
android:minWidth="50dp"
android:text="@string/recipient_from"
style="@style/ComposeTextLabel" />
style="@style/ComposeTextLabel"
/>
<TextView
android:id="@+id/identity"
android:layout_width="fill_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/from_label"
android:layout_toEndOf="@id/from_label"
android:layout_marginRight="36dp"
android:layout_marginEnd="36dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:id="@+id/identity"
android:singleLine="true"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:ellipsize="end"
tools:text="Address"
style="@style/ComposeEditText"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_gravity="center_vertical"
android:id="@+id/pgp_inline_indicator"
android:src="@drawable/compatibility"
android:tint="@color/light_black"
android:visibility="gone"
tools:visibility="visible"
/>
<com.fsck.k9.view.ToolableViewAnimator
android:layout_width="36dp"
android:layout_height="32dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_gravity="center_vertical"
android:id="@+id/crypto_status"
android:visibility="gone"
tools:visibility="visible"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
custom:previewInitialChild="2"
tools:visibility="visible">
custom:previewInitialChild="2">
<ImageView
android:layout_width="wrap_content"
@ -203,7 +215,7 @@
</com.fsck.k9.view.ToolableViewAnimator>
</RelativeLayout>
</LinearLayout>
<View
android:layout_width="match_parent"

View file

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="10dp"
tools:ignore="UseCompoundDrawables"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/compatibility"
android:scaleType="fitCenter"
android:tint="@color/light_black"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/openpgp_inline_title"
/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceSmall"
android:layout_marginBottom="4dp"
android:text="@string/openpgp_inline_text"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/PositiveBulletPoint"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/openpgp_inline_plus_compat"
/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/NegativeBulletPoint"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/openpgp_inline_minus_transit"
/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/NegativeBulletPoint"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/openpgp_inline_minus_attach"
/>
</LinearLayout>
</LinearLayout>

View file

@ -37,4 +37,16 @@
android:title="@string/read_receipt"
android:icon="?attr/iconActionRequestReadReceipt"
/>
<item
android:id="@+id/openpgp_inline_enable"
android:alphabeticShortcut="i"
android:title="@string/enable_inline_pgp"
android:showAsAction="never"
/>
<item
android:id="@+id/openpgp_inline_disable"
android:alphabeticShortcut="i"
android:title="@string/disable_inline_pgp"
android:showAsAction="never"
/>
</menu>

View file

@ -54,6 +54,8 @@
<attr name="composerBackgroundColor" format="color"/>
<attr name="contactPictureFallbackDefaultBackgroundColor" format="reference|color"/>
<attr name="contactTokenBackgroundColor" format="reference|color"/>
<attr name="tintColorBulletPointPositive" format="reference|color"/>
<attr name="tintColorBulletPointNegative" format="reference|color"/>
</declare-styleable>
<declare-styleable name="SliderPreference">

View file

@ -2,6 +2,8 @@
<resources>
<color name="message_list_item_footer_background">#eeeeee</color>
<color name="light_black">#444</color>
<color name="openpgp_red">#CC0000</color>
<color name="openpgp_orange">#FF8800</color>
<color name="openpgp_green">#669900</color>

View file

@ -1152,5 +1152,16 @@ Please submit bug reports, contribute new features and ask questions at
<string name="crypto_mode_private">Always Sign, Always Encrypt.</string>
<string name="error_crypto_provider_connect">Cannot connect to crypto provider, check your settings or click crypto icon to retry!</string>
<string name="error_crypto_provider_ui_required">Crypto provider access denied, click crypto icon to retry!</string>
<string name="error_crypto_inline_attach">PGP/INLINE mode does not support attachments!</string>
<string name="enable_inline_pgp">Enable PGP/INLINE</string>
<string name="disable_inline_pgp">Disable PGP/INLINE</string>
<string name="openpgp_inline_title">PGP/INLINE Mode</string>
<string name="openpgp_inline_text">The email is sent in PGP/INLINE format.\nThis should only be used for compatibility:</string>
<string name="openpgp_inline_plus_compat">Some clients only support this format</string>
<string name="openpgp_inline_minus_transit">Signatures may break during transit</string>
<string name="openpgp_inline_minus_attach">Attachments are not supported</string>
<string name="openpgp_inline_ok">Got it!</string>
<string name="openpgp_inline_disable">Disable</string>
<string name="openpgp_inline_keep_enabled">Keep Enabled</string>
</resources>

View file

@ -51,5 +51,19 @@
<item name="android:textColorHint">#aaa</item>
</style>
<style name="ShowcaseTheme" parent="ShowcaseView.Light">
<item name="sv_backgroundColor">#99444444</item>
</style>
<style name="PositiveBulletPoint">
<item name="android:src">@drawable/bullet_point_positive</item>
<item name="android:tint">?attr/tintColorBulletPointPositive</item>
</style>
<style name="NegativeBulletPoint">
<item name="android:src">@drawable/bullet_point_negative</item>
<item name="android:tint">?attr/tintColorBulletPointNegative</item>
</style>
</resources>

View file

@ -61,6 +61,8 @@
<item name="contactPictureFallbackDefaultBackgroundColor">#ffababab</item>
<item name="contactTokenBackgroundColor">#ccc</item>
<item name="composerBackgroundColor">@android:color/background_light</item>
<item name="tintColorBulletPointPositive">#77aa22</item>
<item name="tintColorBulletPointNegative">#dd2222</item>
</style>
<style name="Theme.K9.Dark" parent="Theme.K9.Dark.Base">
@ -115,6 +117,8 @@
<item name="contactTokenBackgroundColor">#313131</item>
<item name="contactPictureFallbackDefaultBackgroundColor">#ff606060</item>
<item name="composerBackgroundColor">@android:color/background_dark</item>
<item name="tintColorBulletPointPositive">#77aa22</item>
<item name="tintColorBulletPointNegative">#dd2222</item>
</style>
<style name="Theme.K9.Dialog.Light" parent="Theme.K9.Light">

View file

@ -0,0 +1,182 @@
package com.fsck.k9.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.Identity;
import com.fsck.k9.activity.misc.Attachment;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.message.MessageBuilder.Callback;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "src/main/AndroidManifest.xml", sdk = 21)
public class MessageBuilderTest {
public static final String TEST_MESSAGE_TEXT = "message text";
public static final String TEST_SUBJECT = "test_subject";
public static final Address TEST_IDENTITY_ADDRESS = new Address("test@example.org", "tester");
public static final Address[] TEST_TO = new Address[] {
new Address("to1@example.org", "recip 1"), new Address("to2@example.org", "recip 2")
};
public static final Address[] TEST_CC = new Address[] { new Address("cc@example.org", "cc recip") };
public static final Address[] TEST_BCC = new Address[] { new Address("bcc@example.org", "bcc recip") };
@Test
public void build__shouldSucceed() throws MessagingException {
MessageBuilder messageBuilder = createSimpleMessageBuilder();
Callback mockCallback = mock(Callback.class);
messageBuilder.buildAsync(mockCallback);
ArgumentCaptor<MimeMessage> mimeMessageCaptor = ArgumentCaptor.forClass(MimeMessage.class);
verify(mockCallback).onMessageBuildSuccess(mimeMessageCaptor.capture(), eq(false));
verifyNoMoreInteractions(mockCallback);
MimeMessage mimeMessage = mimeMessageCaptor.getValue();
assertContentOfBodyEquals("message content must match", mimeMessage.getBody(), TEST_MESSAGE_TEXT);
assertEquals("text/plain", mimeMessage.getMimeType());
assertEquals(TEST_SUBJECT, mimeMessage.getSubject());
assertEquals(TEST_IDENTITY_ADDRESS, mimeMessage.getFrom()[0]);
assertArrayEquals(TEST_TO, mimeMessage.getRecipients(RecipientType.TO));
assertArrayEquals(TEST_CC, mimeMessage.getRecipients(RecipientType.CC));
assertArrayEquals(TEST_BCC, mimeMessage.getRecipients(RecipientType.BCC));
}
@Test
public void build__detachAndReattach__shouldSucceed() throws MessagingException {
MessageBuilder messageBuilder = createSimpleMessageBuilder();
Callback mockCallback = mock(Callback.class);
Robolectric.getBackgroundThreadScheduler().pause();
messageBuilder.buildAsync(mockCallback);
messageBuilder.detachCallback();
Robolectric.getBackgroundThreadScheduler().unPause();
verifyNoMoreInteractions(mockCallback);
mockCallback = mock(Callback.class);
messageBuilder.reattachCallback(mockCallback);
verify(mockCallback).onMessageBuildSuccess(any(MimeMessage.class), eq(false));
verifyNoMoreInteractions(mockCallback);
}
@Test
public void buildWithException__shouldThrow() throws MessagingException {
MessageBuilder messageBuilder = new SimpleMessageBuilder(RuntimeEnvironment.application) {
@Override
protected void buildMessageInternal() {
queueMessageBuildException(new MessagingException("expected error"));
}
};
Callback mockCallback = mock(Callback.class);
messageBuilder.buildAsync(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
}
@Test
public void buildWithException__detachAndReattach__shouldThrow() throws MessagingException {
MessageBuilder messageBuilder = new SimpleMessageBuilder(RuntimeEnvironment.application) {
@Override
protected void buildMessageInternal() {
queueMessageBuildException(new MessagingException("expected error"));
}
};
Callback mockCallback = mock(Callback.class);
Robolectric.getBackgroundThreadScheduler().pause();
messageBuilder.buildAsync(mockCallback);
messageBuilder.detachCallback();
Robolectric.getBackgroundThreadScheduler().unPause();
verifyNoMoreInteractions(mockCallback);
mockCallback = mock(Callback.class);
messageBuilder.reattachCallback(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
}
private static MessageBuilder createSimpleMessageBuilder() {
MessageBuilder b = new SimpleMessageBuilder(RuntimeEnvironment.application);
Identity identity = new Identity();
identity.setName(TEST_IDENTITY_ADDRESS.getPersonal());
identity.setEmail(TEST_IDENTITY_ADDRESS.getAddress());
identity.setDescription("test identity");
identity.setSignatureUse(false);
b.setSubject(TEST_SUBJECT)
.setTo(Arrays.asList(TEST_TO))
.setCc(Arrays.asList(TEST_CC))
.setBcc(Arrays.asList(TEST_BCC))
.setInReplyTo("inreplyto")
.setReferences("references")
.setRequestReadReceipt(false)
.setIdentity(identity)
.setMessageFormat(SimpleMessageFormat.TEXT)
.setText(TEST_MESSAGE_TEXT)
.setAttachments(new ArrayList<Attachment>())
.setSignature("signature")
.setQuoteStyle(QuoteStyle.PREFIX)
.setQuotedTextMode(QuotedTextMode.NONE)
.setQuotedText("quoted text")
.setQuotedHtmlContent(new InsertableHtmlContent())
.setReplyAfterQuote(false)
.setSignatureBeforeQuotedText(false)
.setIdentityChanged(false)
.setSignatureChanged(false)
.setCursorPosition(0)
.setMessageReference(null)
.setDraft(false);
return b;
}
private static void assertContentOfBodyEquals(String reason, Body bodyPart, String expected) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bodyPart.writeTo(bos) ;
Assert.assertEquals(reason, expected, new String(bos.toByteArray()));
} catch (IOException | MessagingException e) {
fail();
}
}
}

View file

@ -0,0 +1,504 @@
package com.fsck.k9.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.Identity;
import com.fsck.k9.activity.compose.ComposeCryptoStatus;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.ComposeCryptoStatusBuilder;
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode;
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoProviderState;
import com.fsck.k9.activity.misc.Attachment;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.MessageBuilder.Callback;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.util.MimeUtil;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "src/main/AndroidManifest.xml", sdk = 21)
public class PgpMessageBuilderTest {
public static final long TEST_SIGN_KEY_ID = 123L;
public static final long TEST_SELF_ENCRYPT_KEY_ID = 234L;
public static final String TEST_MESSAGE_TEXT = "message text with a ☭ CCCP symbol";
private ComposeCryptoStatusBuilder cryptoStatusBuilder = createDefaultComposeCryptoStatusBuilder();
private OpenPgpApi openPgpApi = mock(OpenPgpApi.class);
private PgpMessageBuilder pgpMessageBuilder = createDefaultPgpMessageBuilder(openPgpApi);
@Test(expected = AssertionError.class)
public void build__withDisabledCrypto__shouldError() throws MessagingException {
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.setCryptoMode(CryptoMode.DISABLE).build());
pgpMessageBuilder.buildAsync(mock(Callback.class));
}
@Test
public void build__withCryptoProviderNotOk__shouldThrow() throws MessagingException {
cryptoStatusBuilder.setCryptoMode(CryptoMode.SIGN_ONLY);
CryptoProviderState[] cryptoProviderStates = {
CryptoProviderState.LOST_CONNECTION, CryptoProviderState.UNCONFIGURED,
CryptoProviderState.UNINITIALIZED, CryptoProviderState.ERROR
};
for (CryptoProviderState state : cryptoProviderStates) {
cryptoStatusBuilder.setCryptoProviderState(state);
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
}
}
@Test
public void buildSign__withNoDetachedSignatureInResult__shouldThrow() throws MessagingException {
cryptoStatusBuilder.setCryptoMode(CryptoMode.SIGN_ONLY);
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
Intent returnIntent = new Intent();
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
}
@Test
public void buildSign__withDetachedSignatureInResult__shouldSucceed() throws MessagingException {
cryptoStatusBuilder.setCryptoMode(CryptoMode.SIGN_ONLY);
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
ArgumentCaptor<Intent> capturedApiIntent = ArgumentCaptor.forClass(Intent.class);
Intent returnIntent = new Intent();
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
returnIntent.putExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE, new byte[] { 1, 2, 3 });
when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
Intent expectedIntent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN);
expectedIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
expectedIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
assertIntentEqualsActionAndExtras(expectedIntent, capturedApiIntent.getValue());
ArgumentCaptor<MimeMessage> captor = ArgumentCaptor.forClass(MimeMessage.class);
verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false));
verifyNoMoreInteractions(mockCallback);
MimeMessage message = captor.getValue();
Assert.assertEquals("message must be multipart/signed", "multipart/signed", message.getMimeType());
MimeMultipart multipart = (MimeMultipart) message.getBody();
Assert.assertEquals("multipart/signed must consist of two parts", 2, multipart.getCount());
BodyPart contentBodyPart = multipart.getBodyPart(0);
Assert.assertEquals("first part must have content type text/plain",
"text/plain", MimeUtility.getHeaderParameter(contentBodyPart.getContentType(), null));
Assert.assertTrue("signed message body must be TextBody", contentBodyPart.getBody() instanceof TextBody);
Assert.assertEquals(MimeUtil.ENC_QUOTED_PRINTABLE, ((TextBody) contentBodyPart.getBody()).getEncoding());
assertContentOfBodyPartEquals("content must match the message text", contentBodyPart, TEST_MESSAGE_TEXT);
BodyPart signatureBodyPart = multipart.getBodyPart(1);
Assert.assertEquals("second part must be pgp signature",
"application/pgp-signature", signatureBodyPart.getContentType());
assertContentOfBodyPartEquals("content must match the supplied detached signature",
signatureBodyPart, new byte[] { 1, 2, 3 });
}
@Test
public void buildSign__withUserInteractionResult__shouldReturnUserInteraction() throws MessagingException {
cryptoStatusBuilder.setCryptoMode(CryptoMode.SIGN_ONLY);
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
Intent returnIntent = mock(Intent.class);
when(returnIntent.getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt()))
.thenReturn(OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
final PendingIntent mockPendingIntent = mock(PendingIntent.class);
when(returnIntent.getParcelableExtra(eq(OpenPgpApi.RESULT_INTENT)))
.thenReturn(mockPendingIntent);
when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
ArgumentCaptor<PendingIntent> captor = ArgumentCaptor.forClass(PendingIntent.class);
verify(mockCallback).onMessageBuildReturnPendingIntent(captor.capture(), anyInt());
verifyNoMoreInteractions(mockCallback);
PendingIntent pendingIntent = captor.getValue();
Assert.assertSame(pendingIntent, mockPendingIntent);
}
@Test
public void buildSign__withReturnAfterUserInteraction__shouldSucceed() throws MessagingException {
cryptoStatusBuilder.setCryptoMode(CryptoMode.SIGN_ONLY);
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
int returnedRequestCode;
{
Intent returnIntent = spy(new Intent());
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
PendingIntent mockPendingIntent = mock(PendingIntent.class);
when(returnIntent.getParcelableExtra(eq(OpenPgpApi.RESULT_INTENT)))
.thenReturn(mockPendingIntent);
when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
verify(returnIntent).getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt());
ArgumentCaptor<PendingIntent> piCaptor = ArgumentCaptor.forClass(PendingIntent.class);
ArgumentCaptor<Integer> rcCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mockCallback).onMessageBuildReturnPendingIntent(piCaptor.capture(), rcCaptor.capture());
verifyNoMoreInteractions(mockCallback);
returnedRequestCode = rcCaptor.getValue();
Assert.assertSame(mockPendingIntent, piCaptor.getValue());
}
{
Intent returnIntent = spy(new Intent());
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
Intent mockReturnIntent = mock(Intent.class);
when(openPgpApi.executeApi(same(mockReturnIntent), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.onActivityResult(mockCallback, returnedRequestCode, Activity.RESULT_OK, mockReturnIntent);
verify(openPgpApi).executeApi(same(mockReturnIntent), any(OpenPgpDataSource.class), any(OutputStream.class));
verify(returnIntent).getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt());
}
}
@Test
public void buildEncrypt__withoutRecipients__shouldThrow() throws MessagingException {
cryptoStatusBuilder
.setCryptoMode(CryptoMode.OPPORTUNISTIC)
.setRecipients(new ArrayList<Recipient>());
pgpMessageBuilder.setCryptoStatus(cryptoStatusBuilder.build());
Intent returnIntent = spy(new Intent());
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
}
@Test
public void buildEncrypt__shouldSucceed() throws MessagingException {
ComposeCryptoStatus cryptoStatus = cryptoStatusBuilder
.setCryptoMode(CryptoMode.PRIVATE)
.setRecipients(Collections.singletonList(new Recipient("test", "test@example.org", "labru", -1, "key")))
.build();
pgpMessageBuilder.setCryptoStatus(cryptoStatus);
ArgumentCaptor<Intent> capturedApiIntent = ArgumentCaptor.forClass(Intent.class);
Intent returnIntent = new Intent();
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
when(openPgpApi.executeApi(capturedApiIntent.capture(),
any(OpenPgpDataSource.class), any(OutputStream.class))).thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, new long[] { TEST_SELF_ENCRYPT_KEY_ID });
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, false);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses());
assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue());
ArgumentCaptor<MimeMessage> captor = ArgumentCaptor.forClass(MimeMessage.class);
verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false));
verifyNoMoreInteractions(mockCallback);
MimeMessage message = captor.getValue();
Assert.assertEquals("message must be multipart/encrypted", "multipart/encrypted", message.getMimeType());
MimeMultipart multipart = (MimeMultipart) message.getBody();
Assert.assertEquals("multipart/encrypted must consist of two parts", 2, multipart.getCount());
BodyPart dummyBodyPart = multipart.getBodyPart(0);
Assert.assertEquals("first part must be pgp encrypted dummy part",
"application/pgp-encrypted", dummyBodyPart.getContentType());
assertContentOfBodyPartEquals("content must match the supplied detached signature",
dummyBodyPart, "Version: 1");
BodyPart encryptedBodyPart = multipart.getBodyPart(1);
Assert.assertEquals("second part must be octet-stream of encrypted data",
"application/octet-stream", encryptedBodyPart.getContentType());
Assert.assertTrue("message body must be BinaryTempFileBody",
encryptedBodyPart.getBody() instanceof BinaryTempFileBody);
Assert.assertEquals(MimeUtil.ENC_7BIT, ((BinaryTempFileBody) encryptedBodyPart.getBody()).getEncoding());
}
@Test
public void buildEncrypt__withInlineEnabled__shouldSucceed() throws MessagingException {
ComposeCryptoStatus cryptoStatus = cryptoStatusBuilder
.setCryptoMode(CryptoMode.PRIVATE)
.setRecipients(Collections.singletonList(new Recipient("test", "test@example.org", "labru", -1, "key")))
.setEnablePgpInline(true)
.build();
pgpMessageBuilder.setCryptoStatus(cryptoStatus);
ArgumentCaptor<Intent> capturedApiIntent = ArgumentCaptor.forClass(Intent.class);
Intent returnIntent = new Intent();
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, new long[] { TEST_SELF_ENCRYPT_KEY_ID });
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, false);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses());
assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue());
ArgumentCaptor<MimeMessage> captor = ArgumentCaptor.forClass(MimeMessage.class);
verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false));
verifyNoMoreInteractions(mockCallback);
MimeMessage message = captor.getValue();
Assert.assertEquals("text/plain", message.getMimeType());
Assert.assertTrue("message body must be BinaryTempFileBody", message.getBody() instanceof BinaryTempFileBody);
Assert.assertEquals(MimeUtil.ENC_7BIT, ((BinaryTempFileBody) message.getBody()).getEncoding());
}
@Test
public void buildSign__withInlineEnabled__shouldSucceed() throws MessagingException {
ComposeCryptoStatus cryptoStatus = cryptoStatusBuilder
.setCryptoMode(CryptoMode.SIGN_ONLY)
.setRecipients(Collections.singletonList(new Recipient("test", "test@example.org", "labru", -1, "key")))
.setEnablePgpInline(true)
.build();
pgpMessageBuilder.setCryptoStatus(cryptoStatus);
ArgumentCaptor<Intent> capturedApiIntent = ArgumentCaptor.forClass(Intent.class);
Intent returnIntent = new Intent();
returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), any(OutputStream.class)))
.thenReturn(returnIntent);
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_SIGN_KEY_ID);
expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue());
ArgumentCaptor<MimeMessage> captor = ArgumentCaptor.forClass(MimeMessage.class);
verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false));
verifyNoMoreInteractions(mockCallback);
MimeMessage message = captor.getValue();
Assert.assertEquals("message must be text/plain", "text/plain", message.getMimeType());
}
@Test
public void buildSignWithAttach__withInlineEnabled__shouldThrow() throws MessagingException {
ComposeCryptoStatus cryptoStatus = cryptoStatusBuilder
.setCryptoMode(CryptoMode.SIGN_ONLY)
.setEnablePgpInline(true)
.build();
pgpMessageBuilder.setCryptoStatus(cryptoStatus);
pgpMessageBuilder.setAttachments(Collections.singletonList(new Attachment()));
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
verifyNoMoreInteractions(openPgpApi);
}
@Test
public void buildEncryptWithAttach__withInlineEnabled__shouldThrow() throws MessagingException {
ComposeCryptoStatus cryptoStatus = cryptoStatusBuilder
.setCryptoMode(CryptoMode.OPPORTUNISTIC)
.setEnablePgpInline(true)
.build();
pgpMessageBuilder.setCryptoStatus(cryptoStatus);
pgpMessageBuilder.setAttachments(Collections.singletonList(new Attachment()));
Callback mockCallback = mock(Callback.class);
pgpMessageBuilder.buildAsync(mockCallback);
verify(mockCallback).onMessageBuildException(any(MessagingException.class));
verifyNoMoreInteractions(mockCallback);
verifyNoMoreInteractions(openPgpApi);
}
private ComposeCryptoStatusBuilder createDefaultComposeCryptoStatusBuilder() {
return new ComposeCryptoStatusBuilder()
.setEnablePgpInline(false)
.setSigningKeyId(TEST_SIGN_KEY_ID)
.setSelfEncryptId(TEST_SELF_ENCRYPT_KEY_ID)
.setRecipients(new ArrayList<Recipient>())
.setCryptoProviderState(CryptoProviderState.OK);
}
private static PgpMessageBuilder createDefaultPgpMessageBuilder(OpenPgpApi openPgpApi) {
PgpMessageBuilder b = new PgpMessageBuilder(RuntimeEnvironment.application);
b.setOpenPgpApi(openPgpApi);
Identity identity = new Identity();
identity.setName("tester");
identity.setEmail("test@example.org");
identity.setDescription("test identity");
identity.setSignatureUse(false);
b.setSubject("subject")
.setTo(new ArrayList<Address>())
.setCc(new ArrayList<Address>())
.setBcc(new ArrayList<Address>())
.setInReplyTo("inreplyto")
.setReferences("references")
.setRequestReadReceipt(false)
.setIdentity(identity)
.setMessageFormat(SimpleMessageFormat.TEXT)
.setText(TEST_MESSAGE_TEXT)
.setAttachments(new ArrayList<Attachment>())
.setSignature("signature")
.setQuoteStyle(QuoteStyle.PREFIX)
.setQuotedTextMode(QuotedTextMode.NONE)
.setQuotedText("quoted text")
.setQuotedHtmlContent(new InsertableHtmlContent())
.setReplyAfterQuote(false)
.setSignatureBeforeQuotedText(false)
.setIdentityChanged(false)
.setSignatureChanged(false)
.setCursorPosition(0)
.setMessageReference(null)
.setDraft(false);
return b;
}
private static void assertContentOfBodyPartEquals(String reason, BodyPart signatureBodyPart, byte[] expected) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
signatureBodyPart.getBody().writeTo(bos);
Assert.assertArrayEquals(reason, expected, bos.toByteArray());
} catch (IOException | MessagingException e) {
Assert.fail();
}
}
private static void assertContentOfBodyPartEquals(String reason, BodyPart signatureBodyPart, String expected) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream inputStream = MimeUtility.decodeBody(signatureBodyPart.getBody());
IOUtils.copy(inputStream, bos);
Assert.assertEquals(reason, expected, new String(bos.toByteArray()));
} catch (IOException | MessagingException e) {
Assert.fail();
}
}
private static void assertIntentEqualsActionAndExtras(Intent expected, Intent actual) {
Assert.assertEquals(expected.getAction(), actual.getAction());
Bundle expectedExtras = expected.getExtras();
Bundle intentExtras = actual.getExtras();
if (expectedExtras.size() != intentExtras.size()) {
Assert.assertEquals(expectedExtras.size(), intentExtras.size());
}
for (String key : expectedExtras.keySet()) {
Object intentExtra = intentExtras.get(key);
Object expectedExtra = expectedExtras.get(key);
if (intentExtra == null) {
if (expectedExtra == null) {
continue;
}
Assert.fail("found null for an expected non-null extra: " + key);
}
if (intentExtra instanceof long[]) {
if (!Arrays.equals((long[]) intentExtra, (long[]) expectedExtra)) {
Assert.assertArrayEquals((long[]) expectedExtra, (long[]) intentExtra);
}
} else {
if (!intentExtra.equals(expectedExtra)) {
Assert.assertEquals(expectedExtra, intentExtra);
}
}
}
}
}