Merge pull request #1331 from k9mail/compose-mime-combined
PGP/INLINE and build in combined mime format
66
images/drawables-pgp/bullet_point_negative.svg
Normal 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 |
65
images/drawables-pgp/bullet_point_positive.svg
Normal 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 |
1
images/drawables-pgp/compatibility.svg
Normal 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 |
54
images/drawables-pgp/docs/disabled.svg
Normal 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 |
67
images/drawables-pgp/docs/signcrypt_confirmed.svg
Normal 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 |
91
images/drawables-pgp/docs/signcrypt_error.svg
Normal 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 |
67
images/drawables-pgp/docs/signcrypt_unconfirmed.svg
Normal 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 |
80
images/drawables-pgp/docs/signcrypt_unknown.svg
Normal 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 |
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -32,10 +32,15 @@ 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 name = MimeUtility.getHeaderParameter(part.getContentType(), "name");
|
||||
if (name != null) {
|
||||
contentType += String.format(";\r\n name=\"%s\"", name);
|
||||
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);
|
||||
|
||||
|
|
|
@ -132,4 +132,8 @@ public class TextBody implements Body, SizeAware {
|
|||
|
||||
return countingOutputStream.getCount();
|
||||
}
|
||||
|
||||
public String getEncoding() {
|
||||
return mEncoding;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
if (!cryptoStatus.isEncryptionEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent encryptIntent = new Intent(OpenPgpApi.ACTION_ENCRYPT);
|
||||
encryptIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
|
||||
long[] encryptKeyIds = cryptoStatus.getEncryptKeyIds();
|
||||
if (encryptKeyIds != null) {
|
||||
encryptIntent.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;
|
||||
@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!");
|
||||
}
|
||||
encryptIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, encryptRecipientAddresses);
|
||||
encryptIntent.putExtra(OpenPgpApi.EXTRA_ENCRYPT_OPPORTUNISTIC, cryptoStatus.isEncryptionOpportunistic());
|
||||
// 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) {
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, encryptKeyIds);
|
||||
}
|
||||
|
||||
if(!isDraft()) {
|
||||
String[] encryptRecipientAddresses = cryptoStatus.getRecipientAddresses();
|
||||
boolean hasRecipientAddresses = encryptRecipientAddresses != null && encryptRecipientAddresses.length > 0;
|
||||
if (!hasRecipientAddresses) {
|
||||
throw new MessagingException("encryption is enabled, but no recipient specified!");
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
pgpApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
|
||||
return pgpApiIntent;
|
||||
}
|
||||
|
||||
private void startOrContinueSigningIfRequested(Intent userInteractionResult) throws MessagingException {
|
||||
boolean reenterOperation = currentState == State.OPENPGP_SIGN;
|
||||
if (reenterOperation) {
|
||||
mimeIntentLaunch(userInteractionResult);
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
throw new IllegalStateException("unreachable code segment reached");
|
||||
}
|
||||
|
||||
private void launchUserInteraction(Intent result) {
|
||||
PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
|
||||
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);
|
||||
} else {
|
||||
throw new IllegalStateException("illegal state!");
|
||||
}
|
||||
@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 {
|
||||
bodyPart.writeTo(os);
|
||||
}
|
||||
} catch (MessagingException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void mimeBuildSignedMessage(BodyPart signedBodyPart, Intent result) throws MessagingException {
|
||||
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!");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
BIN
k9mail/src/main/res/drawable-hdpi/bullet_point_negative.png
Normal file
After Width: | Height: | Size: 423 B |
BIN
k9mail/src/main/res/drawable-hdpi/bullet_point_positive.png
Normal file
After Width: | Height: | Size: 499 B |
BIN
k9mail/src/main/res/drawable-hdpi/compatibility.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
k9mail/src/main/res/drawable-mdpi/bullet_point_negative.png
Normal file
After Width: | Height: | Size: 336 B |
BIN
k9mail/src/main/res/drawable-mdpi/bullet_point_positive.png
Normal file
After Width: | Height: | Size: 394 B |
BIN
k9mail/src/main/res/drawable-mdpi/compatibility.png
Normal file
After Width: | Height: | Size: 500 B |
BIN
k9mail/src/main/res/drawable-xhdpi/bullet_point_negative.png
Normal file
After Width: | Height: | Size: 578 B |
BIN
k9mail/src/main/res/drawable-xhdpi/bullet_point_positive.png
Normal file
After Width: | Height: | Size: 663 B |
BIN
k9mail/src/main/res/drawable-xhdpi/compatibility.png
Normal file
After Width: | Height: | Size: 875 B |
BIN
k9mail/src/main/res/drawable-xxhdpi/bullet_point_negative.png
Normal file
After Width: | Height: | Size: 740 B |
BIN
k9mail/src/main/res/drawable-xxhdpi/bullet_point_positive.png
Normal file
After Width: | Height: | Size: 869 B |
BIN
k9mail/src/main/res/drawable-xxhdpi/compatibility.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
|
@ -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"
|
||||
|
|
119
k9mail/src/main/res/layout/openpgp_inline_dialog.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
182
k9mail/src/test/java/com/fsck/k9/message/MessageBuilderTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|