1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-27 03:32:16 -05:00

Merge branch 'pgp_mime_preparations'

This commit is contained in:
cketti 2015-03-16 16:14:45 +01:00
commit ab964cf8af
213 changed files with 7384 additions and 4384 deletions

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>lock-closed</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="lock-closed" sketch:type="MSArtboardGroup" fill="#000000">
<path d="M81.502,45.132 L79.577,45.132 L79.577,29.479 C79.479,10.285 66.387,-0.164 50.476,-0.164 C34.57,-0.164 20.304,10.782 20.801,29.479 L20.785,45.112 C20.785,45.112 21.025,45.133 19.825,45.133 C18.555,45.133 10.185,46.606 10.185,54.069 L10.185,89.893 C10.185,97.852 19.605,99.836 19.825,99.836 L81.027,99.836 C81.247,99.836 90.181,98.843 90.181,89.893 L90.181,54.564 C90.182,46.109 81.727,45.132 81.502,45.132 L81.502,45.132 Z M59.334,86.055 L41.061,86.055 L46.024,71.489 C43.904,70.077 42.496,67.623 42.496,64.824 C42.496,60.44 45.938,56.886 50.183,56.886 C54.428,56.886 57.87,60.443 57.87,64.824 C57.87,67.619 56.466,70.077 54.348,71.485 L59.334,86.055 L59.334,86.055 Z M34.261,45.132 L34.277,29.686 C34.277,19.737 40.348,11.783 50.183,11.783 C59.924,11.783 66.088,18.741 66.088,29.686 L66.098,45.132 L34.261,45.132 L34.261,45.132 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>lock-error</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="lock-error" sketch:type="MSArtboardGroup" fill="#000000">
<path d="M80.459,45.474 L78.533,45.474 L78.533,29.826 C78.435,10.633 65.344,0.183 49.433,0.183 C33.527,0.183 19.265,11.128 19.761,29.826 L19.745,45.454 C19.745,45.454 19.985,45.475 18.784,45.475 C17.514,45.475 9.145,46.946 9.145,54.407 L9.145,90.228 C9.145,98.187 18.565,100.171 18.784,100.171 L79.984,100.171 C80.203,100.171 89.138,99.178 89.138,90.228 L89.138,54.901 C89.139,46.452 80.684,45.474 80.459,45.474 L80.459,45.474 Z M33.234,30.033 C33.234,20.084 39.304,12.131 49.14,12.131 C58.881,12.131 65.045,19.088 65.045,30.033 L65.055,45.474 L33.218,45.474 L33.234,30.033 L33.234,30.033 Z M59.4033767,90.873 L48.4582822,79.9279055 L38.2296593,90.3644491 L31.6365,83.7568884 L42.5824946,72.8153942 L32.3439707,62.5939721 L38.7544118,56.1079235 L49.7013065,67.0503177 L60.1234487,56.7100837 L66.6365,63.2240351 L55.6896053,74.1673294 L66.0091373,84.4778605 L59.4033767,90.873 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>lock-open</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="lock-open" sketch:type="MSArtboardGroup" fill="#000000">
<path d="M79.577,26.479 C79.577,10.833 66.387,-0.164 50.476,-0.164 C34.57,-0.164 20.304,10.782 20.801,29.479 L20.785,45.112 C20.785,45.112 21.025,45.133 19.825,45.133 C18.555,45.133 10.185,46.606 10.185,54.069 L10.185,89.893 C10.185,97.852 19.605,99.836 19.825,99.836 L81.027,99.836 C81.247,99.836 90.181,98.843 90.181,89.893 L90.181,54.564 C90.181,46.107 81.726,45.13 81.5,45.13 L34.259,45.13 L34.275,29.684 C34.275,19.735 40.346,11.781 50.181,11.781 C59.922,11.781 66.664,18.164 66.664,29.164 L79.577,26.479 Z M59.334,86.055 L41.061,86.055 L46.024,71.49 C43.904,70.078 42.496,67.624 42.496,64.825 C42.496,60.44 45.938,56.887 50.183,56.887 C54.428,56.887 57.87,60.445 57.87,64.825 C57.87,67.62 56.466,70.078 54.348,71.485 L59.334,86.055 L59.334,86.055 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-expired-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-expired-cutout" sketch:type="MSArtboardGroup" transform="translate(0.110156, 0.000000)" fill="#000000">
<path d="M5.21763502,25.9334098 C4.62201801,24.5421709 5.31408066,20.2649627 6.50270803,18.5737297 C7.69394204,16.8824967 14.8139764,11.1118682 18.0827017,9.51888655 C21.3514269,7.92852492 25.1232335,10.7136228 25.1232335,10.7136228 L31.1693326,18.5737297 C21.6564037,21.754453 12.2477403,33.889148 12.2477403,33.889148 C12.2477403,33.889148 5.81325202,27.3259588 5.21763502,25.9334098 Z M50.7969868,98.0040129 C30.5564763,98.0040129 14.1592664,81.3860653 14.1592664,60.910451 C14.1592664,41.4138456 29.0387981,25.4459175 47.9423376,23.9707712 L47.9423376,18.3341735 L41.6333009,18.3341735 L41.6333009,9.02828206 L59.2561767,9.02828206 L59.2561767,18.3341735 L53.6477076,18.3341735 L53.6477076,23.9707712 C72.5460092,25.4445909 87.4333977,41.412519 87.4333977,60.910451 C87.4333977,81.3847387 71.0296405,98.0040129 50.7969868,98.0040129 Z M51.541054,71.6933659 C57.6539179,71.6933659 62.6093732,66.7455263 62.6093732,60.6420567 C62.6093732,54.5385872 57.6539179,49.5907476 51.541054,49.5907476 C45.4281901,49.5907476 40.4727348,54.5385872 40.4727348,60.6420567 C40.4727348,66.7455263 45.4281901,71.6933659 51.541054,71.6933659 Z M96.3766201,25.9341425 C95.7811759,27.3252533 89.3433427,33.889148 89.3433427,33.889148 C89.3433427,33.889148 79.9321974,21.7542607 70.4233315,18.5751403 L76.4676764,10.7157573 C76.4676764,10.7157573 80.2396916,7.92829614 83.5087714,9.5185113 C86.7765483,11.1152759 93.8997286,16.884063 95.0880111,18.5751403 C96.2775965,20.2635977 96.9694584,24.540412 96.3766201,25.9341425 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-invalid-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-invalid-cutout" sketch:type="MSArtboardGroup" transform="translate(0.110156, 0.000000)" fill="#000000">
<path d="M77.3119658,92 L50,64.6787909 L22.6865385,92 L8.00299145,77.3054987 L35.3149573,49.9977557 L8,22.6870202 L22.6850427,8.00149623 L50,35.3137279 L77.3149573,8 L92,22.6825315 L64.6850427,49.9977557 L91.9970085,77.3054987 L77.3119658,92 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 995 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="101px" height="100px" viewBox="0 0 101 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-revoked-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-revoked-cutout" sketch:type="MSArtboardGroup" transform="translate(0.915625, 0.000000)" fill="#000000">
<path d="M50.1457786,95.2902674 C25.2543974,95.2902674 5,75.0407401 5,50.1451337 C5,25.252107 25.2556872,5 50.1457786,5 C75.03587,5 95.2902674,25.252107 95.2902674,50.1451337 C95.2902674,75.0394503 75.0371599,95.2902674 50.1457786,95.2902674 Z M35.5297191,75.6701923 C39.8404345,78.1467253 44.8296167,79.569442 50.1464236,79.569442 C66.3793238,79.569442 79.5862102,66.3638454 79.5862102,50.1296554 C79.5862102,44.8115586 78.1622037,39.8223764 75.6843808,35.5116611 L35.5297191,75.6701923 Z M50.1464236,20.6911586 C33.9135233,20.6911586 20.7066369,33.8967551 20.7066369,50.1309452 C20.7066369,55.3523024 22.0803389,60.2563538 24.473031,64.512895 L64.5296632,24.4575526 C60.2718321,22.0635707 55.3690706,20.6911586 50.1464236,20.6911586 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="101px" height="100px" viewBox="0 0 101 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-unknown-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-unknown-cutout" sketch:type="MSArtboardGroup" transform="translate(0.915625, 0.000000)" fill="#000000">
<path d="M11.4743662,97.2253545 C1.98936285,97.2253571 -1.69987039,86.6466353 1.98936288,81.2764443 C2.36018089,80.2888073 37.5445854,9.4248374 37.6406733,9.21698534 C41.524789,0.483122973 56.8650161,0.0416071437 60.7924391,9.21698534 C60.7572519,9.19524917 98.2991929,81.8687547 97.9337883,81.2642177 C101.323931,86.2404407 96.9260512,97.2253571 88.8978453,97.2253545 C88.8978453,97.2253545 11.4756386,97.2879401 11.4743662,97.2253545 Z M50.5378687,73.3388569 C47.2443918,73.3388569 44.2703808,76.046195 44.2703808,79.5061732 C44.2703808,82.9729198 47.1388056,85.6802579 50.5378687,85.6802579 C53.9369317,85.6802579 56.8040029,82.9729198 56.8040029,79.5061732 C56.8053565,76.046195 53.8313455,73.3388569 50.5378687,73.3388569 Z M50.3063913,28.5 C46.5729719,28.5 42.719076,30.2990258 43.0805057,32.9143334 L45.8826007,65.934287 L54.7315355,65.934287 L57.5322768,32.9143334 C57.8937065,30.2990258 54.0398106,28.5 50.3063913,28.5 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="101px" height="100px" viewBox="0 0 101 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-unverified-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-unverified-cutout" sketch:type="MSArtboardGroup" transform="translate(0.915625, 0.000000)" fill="#000000">
<path d="M49.8900274,96.5521596 C75.8474106,96.5521596 96.8900274,75.5095428 96.8900274,49.5521596 C96.8900274,23.5947764 75.8474106,2.5521596 49.8900274,2.5521596 C23.9326441,2.5521596 2.89002736,23.5947764 2.89002736,49.5521596 C2.89002736,75.5095428 23.9326441,96.5521596 49.8900274,96.5521596 Z M42.9188472,79.4349375 L42.9188472,67.0146143 L55.3391704,67.0146143 L55.3391704,79.4349375 L42.9188472,79.4349375 Z M68.652586,41.8646591 C67.9712562,43.583078 67.1302842,45.0524085 66.1249189,46.2716815 C65.1167028,47.4919237 64.0039592,48.5318919 62.7800362,49.3906167 C61.5570634,50.25128 60.4006082,51.1080664 59.3135213,51.9677605 C58.2254842,52.829393 57.2609796,53.8121774 56.4181072,54.9209598 C55.5733342,56.0307115 55.0449948,57.4147511 54.8273874,59.0779247 L54.8273874,62.237567 L43.8168316,62.237567 L43.8168316,58.4954263 C43.9802747,56.114064 44.4278428,54.1165111 45.1623867,52.5095519 C45.89503,50.9025928 46.7531065,49.5292145 47.7309145,48.3923248 C48.7106231,47.2564044 49.7425954,46.2716815 50.8296823,45.4400947 C51.9177195,44.6104463 52.9230848,43.7769211 53.8476788,42.9463035 C54.7722728,42.1147166 55.5201202,41.2007465 56.0912209,40.2024546 C56.6623216,39.2041628 56.9188893,37.9577517 56.8647251,36.4612832 C56.8647251,33.9112774 56.2537138,32.0261534 55.030741,30.804942 C53.8058678,29.5876075 52.1058691,28.9760325 49.9326456,28.9760325 C48.4645081,28.9760325 47.2006746,29.2677664 46.1392445,29.8492956 C45.0797149,30.4317941 44.2092851,31.2071664 43.5298558,32.177351 C42.848526,33.1475357 42.3477439,34.2844253 42.0208576,35.5860816 C41.6939713,36.8906456 41.5305282,38.2901926 41.5305282,39.7866612 L29.5431146,39.7866612 C29.5953784,36.7917856 30.0999615,34.0479368 31.0521128,31.5541455 C32.0023636,29.0593851 33.3346152,26.8970655 35.0479174,25.0691252 C36.7612195,23.2382773 38.8270647,21.8115922 41.2464032,20.7832547 C43.6666919,19.7568556 46.3730062,19.2441406 49.3624951,19.2441406 C53.2224138,19.2441406 56.4447142,19.785932 59.0274958,20.8666072 C61.6093272,21.9482516 63.6894262,23.2935226 65.2668425,24.9024202 C66.8442588,26.5103485 67.9712562,28.2423365 68.6516357,30.0993532 C69.3310651,31.9573391 69.6703046,33.6883579 69.6703046,35.2962862 C69.6722051,37.9567825 69.3320153,40.1481786 68.652586,41.8646591 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-verified-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-verified-cutout" sketch:type="MSArtboardGroup" transform="translate(0.110156, 0.000000)" fill="#000000">
<path d="M50,97 C75.9573832,97 97,75.9573832 97,50 C97,24.0426168 75.9573832,3 50,3 C24.0426168,3 3,24.0426168 3,50 C3,75.9573832 24.0426168,97 50,97 Z M46.2732912,77.5085 L20,57.830916 L27.9184401,47.6349702 L43.3096859,59.5152262 L70.31112,23 L80.867825,30.7782191 L46.2732912,77.5085 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

19
images/update-drawables-pgp.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
APP_DIR=../k9mail/src/main
MDPI_DIR=$APP_DIR/res/drawable-mdpi
HDPI_DIR=$APP_DIR/res/drawable-hdpi
XDPI_DIR=$APP_DIR/res/drawable-xhdpi
XXDPI_DIR=$APP_DIR/res/drawable-xxhdpi
XXXDPI_DIR=$APP_DIR/res/drawable-xxxhdpi
SRC_DIR=./drawables-pgp/
for NAME in "status_lock_closed" "status_lock_error" "status_lock_open" "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"
inkscape -w 32 -h 32 -e "$HDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
inkscape -w 48 -h 48 -e "$XDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
inkscape -w 64 -h 64 -e "$XXDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
done

View File

@ -2,14 +2,25 @@ package com.fsck.k9.mail;
public abstract class BodyPart implements Part {
private Multipart mParent;
private String serverExtra;
private Multipart parent;
@Override
public String getServerExtra() {
return serverExtra;
}
@Override
public void setServerExtra(String serverExtra) {
this.serverExtra = serverExtra;
}
public Multipart getParent() {
return mParent;
return parent;
}
public void setParent(Multipart parent) {
mParent = parent;
this.parent = parent;
}
public abstract void setEncoding(String encoding) throws MessagingException;

View File

@ -120,9 +120,6 @@ public abstract class Message implements Part, CompositeBody {
@Override
public abstract Body getBody();
@Override
public abstract String getContentType() throws MessagingException;
@Override
public abstract void addHeader(String name, String value) throws MessagingException;
@ -141,7 +138,7 @@ public abstract class Message implements Part, CompositeBody {
public abstract void removeHeader(String name) throws MessagingException;
@Override
public abstract void setBody(Body body) throws MessagingException;
public abstract void setBody(Body body);
public abstract long getId();
@ -150,55 +147,6 @@ public abstract class Message implements Part, CompositeBody {
public abstract int getSize();
/*
* calculateContentPreview
* Takes a plain text message body as a string.
* Returns a message summary as a string suitable for showing in a message list
*
* A message summary should be about the first 160 characters
* of unique text written by the message sender
* Quoted text, "On $date" and so on will be stripped out.
* All newlines and whitespace will be compressed.
*
*/
public static String calculateContentPreview(String text) {
if (text == null) {
return null;
}
// Only look at the first 8k of a message when calculating
// the preview. This should avoid unnecessary
// memory usage on large messages
if (text.length() > 8192) {
text = text.substring(0, 8192);
}
// Remove (correctly delimited by '-- \n') signatures
text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", "");
// try to remove lines of dashes in the preview
text = text.replaceAll("(?m)^----.*?$", "");
// remove quoted text from the preview
text = text.replaceAll("(?m)^[#>].*$", "");
// Remove a common quote header from the preview
text = text.replaceAll("(?m)^On .*wrote.?$", "");
// Remove a more generic quote header from the preview
text = text.replaceAll("(?m)^.*\\w+:$", "");
// Remove horizontal rules.
text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " ");
// URLs in the preview should just be shown as "..." - They're not
// clickable and they usually overwhelm the preview
text = text.replaceAll("https?://\\S+", "...");
// Don't show newlines in the preview
text = text.replaceAll("(\\r|\\n)+", " ");
// Collapse whitespace in the preview
text = text.replaceAll("\\s+", " ");
// Remove any whitespace at the beginning and end of the string.
text = text.trim();
return (text.length() <= 512) ? text : text.substring(0, 512);
}
public void delete(String trashFolderName) throws MessagingException {}
/*

View File

@ -28,7 +28,9 @@ public abstract class Multipart implements CompositeBody {
return Collections.unmodifiableList(mParts);
}
public abstract String getContentType();
public abstract String getMimeType();
public abstract String getBoundary();
public int getCount() {
return mParts.size();
@ -64,4 +66,7 @@ public abstract class Multipart implements CompositeBody {
((TextBody)body).setCharset(charset);
}
}
public abstract byte[] getPreamble();
public abstract byte[] getEpilogue();
}

View File

@ -2,6 +2,7 @@
package com.fsck.k9.mail;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface Part {
@ -15,22 +16,24 @@ public interface Part {
Body getBody();
String getContentType() throws MessagingException;
String getContentType();
String getDisposition() throws MessagingException;
String getContentId() throws MessagingException;
String getContentId();
String[] getHeader(String name) throws MessagingException;
boolean isMimeType(String mimeType) throws MessagingException;
String getMimeType() throws MessagingException;
String getMimeType();
void setBody(Body body) throws MessagingException;
void setBody(Body body);
void writeTo(OutputStream out) throws IOException, MessagingException;
void writeHeaderTo(OutputStream out) throws IOException, MessagingException;
/**
* Called just prior to transmission, once the type of transport is known to
* be 7bit.
@ -44,4 +47,8 @@ public interface Part {
*/
//TODO perhaps it would be clearer to use a flag "force7bit" in writeTo
void setUsing7bitTransport() throws MessagingException;
String getServerExtra();
void setServerExtra(String serverExtra);
}

View File

@ -1,6 +1,5 @@
package com.fsck.k9.mail.internet;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.Base64OutputStream;
import org.apache.commons.io.IOUtils;
@ -15,7 +14,7 @@ import java.io.*;
* and writeTo one time. After writeTo is called, or the InputStream returned from
* getInputStream is closed the file is deleted and the Body should be considered disposed of.
*/
public class BinaryTempFileBody implements RawDataBody {
public class BinaryTempFileBody implements RawDataBody, SizeAware {
private static File mTempDirectory;
private File mFile;
@ -26,6 +25,10 @@ public class BinaryTempFileBody implements RawDataBody {
mTempDirectory = tempDirectory;
}
public static File getTempDirectory() {
return mTempDirectory;
}
@Override
public String getEncoding() {
return mEncoding;
@ -101,6 +104,15 @@ public class BinaryTempFileBody implements RawDataBody {
}
}
@Override
public long getSize() {
return mFile.length();
}
public File getFile() {
return mFile;
}
class BinaryTempFileBodyInputStream extends FilterInputStream {
public BinaryTempFileBodyInputStream(InputStream in) {
super(in);

View File

@ -11,10 +11,7 @@ import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException;
/**
* A {@link BinaryTempFileBody} extension containing a body of type
* message/rfc822. This relates to a BinaryTempFileBody the same way that a
* {@link LocalAttachmentMessageBody} relates to a {@link LocalAttachmentBody}.
*
* A {@link BinaryTempFileBody} extension containing a body of type message/rfc822.
*/
public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody {
@ -63,4 +60,4 @@ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements Com
*/
}
}
}

View File

@ -48,7 +48,7 @@ public class MessageExtractor {
* determine the charset from HTML message.
*/
if (mimeType.equalsIgnoreCase("text/html") && charset == null) {
InputStream in = part.getBody().getInputStream();
InputStream in = MimeUtility.decodeBody(body);
try {
byte[] buf = new byte[256];
in.read(buf, 0, buf.length);
@ -64,18 +64,8 @@ public class MessageExtractor {
}
} finally {
try {
if (in instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) {
/*
* If this is a BinaryTempFileBodyInputStream, calling close()
* will delete the file. But we can't let that happen because
* the file needs to be opened again by the code a few lines
* down.
*/
((BinaryTempFileBody.BinaryTempFileBodyInputStream) in).closeWithoutDeleting();
} else {
in.close();
}
} catch (Exception e) { /* ignore */ }
MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in);
} catch (IOException e) { /* ignore */ }
}
}
charset = fixupCharset(charset, getMessageFromPart(part));
@ -84,22 +74,12 @@ public class MessageExtractor {
* Now we read the part into a buffer for further processing. Because
* the stream is now wrapped we'll remove any transfer encoding at this point.
*/
InputStream in = part.getBody().getInputStream();
InputStream in = MimeUtility.decodeBody(body);
try {
String text = CharsetSupport.readToString(in, charset);
// Replace the body with a TextBody that already contains the decoded text
part.setBody(new TextBody(text));
return text;
return CharsetSupport.readToString(in, charset);
} finally {
try {
/*
* This time we don't care if it's a BinaryTempFileBodyInputStream. We
* replaced the body with a TextBody instance and hence don't need the
* file anymore.
*/
in.close();
MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in);
} catch (IOException e) { /* Ignore */ }
}
}
@ -186,6 +166,8 @@ public class MessageExtractor {
Html html = new Html(part);
viewables.add(html);
}
} else if (part.getMimeType().equalsIgnoreCase("application/pgp-signature")) {
// ignore this type explicitly
} else {
// Everything else is treated as attachment.
attachments.add(part);

View File

@ -5,7 +5,6 @@ import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import java.io.BufferedWriter;
import java.io.IOException;
@ -73,7 +72,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
public void setBody(Body body) throws MessagingException {
public void setBody(Body body) {
this.mBody = body;
}
@ -86,7 +85,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
public String getContentType() throws MessagingException {
public String getContentType() {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType;
}
@ -97,7 +96,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
public String getContentId() throws MessagingException {
public String getContentId() {
String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
if (contentId == null) {
return null;
@ -112,7 +111,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
public String getMimeType() throws MessagingException {
public String getMimeType() {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
@ -135,6 +134,11 @@ public class MimeBodyPart extends BodyPart {
}
}
@Override
public void writeHeaderTo(OutputStream out) throws IOException, MessagingException {
mHeader.writeTo(out);
}
@Override
public void setUsing7bitTransport() throws MessagingException {
String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);

View File

@ -11,28 +11,11 @@ import java.util.*;
public class MimeHeader {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
/**
* Application specific header that contains Store specific information about an attachment.
* In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
* retrieve the attachment at will from the server.
* The info is recorded from this header on LocalStore.appendMessages and is put back
* into the MIME data by LocalStore.fetch.
*/
public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
public static final String HEADER_CONTENT_ID = "Content-ID";
/**
* Fields that should be omitted when writing the header using writeTo()
*/
private static final String[] writeOmitFields = {
// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
// HEADER_ANDROID_ATTACHMENT_ID,
HEADER_ANDROID_ATTACHMENT_STORE_DATA
};
private List<Field> mFields = new ArrayList<Field>();
private String mCharset = null;
@ -101,14 +84,12 @@ public class MimeHeader {
public void writeTo(OutputStream out) throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
for (Field field : mFields) {
if (!Arrays.asList(writeOmitFields).contains(field.name)) {
if (field.hasRawData()) {
writer.write(field.getRaw());
} else {
writeNameValueField(writer, field);
}
writer.write("\r\n");
if (field.hasRawData()) {
writer.write(field.getRaw());
} else {
writeNameValueField(writer, field);
}
writer.write("\r\n");
}
writer.flush();
}

View File

@ -57,6 +57,7 @@ public class MimeMessage extends Message {
private Body mBody;
protected int mSize;
private String serverExtra;
public MimeMessage() {
}
@ -162,7 +163,7 @@ public class MimeMessage extends Message {
}
@Override
public String getContentType() throws MessagingException {
public String getContentType() {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType;
}
@ -171,12 +172,14 @@ public class MimeMessage extends Message {
public String getDisposition() throws MessagingException {
return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
}
@Override
public String getContentId() throws MessagingException {
public String getContentId() {
return null;
}
@Override
public String getMimeType() throws MessagingException {
public String getMimeType() {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
@ -308,13 +311,10 @@ public class MimeMessage extends Message {
if (mMessageId == null) {
mMessageId = getFirstHeader("Message-ID");
}
if (mMessageId == null) { // even after checking the header
setMessageId(generateMessageId());
}
return mMessageId;
}
private String generateMessageId() {
public void generateMessageId() throws MessagingException {
String hostname = null;
if (mFrom != null && mFrom.length >= 1) {
@ -330,7 +330,9 @@ public class MimeMessage extends Message {
}
/* We use upper case here to match Apple Mail Message-ID format (for privacy) */
return "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">";
String messageId = "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">";
setMessageId(messageId);
}
public void setMessageId(String messageId) throws MessagingException {
@ -394,7 +396,7 @@ public class MimeMessage extends Message {
}
@Override
public void setBody(Body body) throws MessagingException {
public void setBody(Body body) {
this.mBody = body;
}
@ -444,6 +446,11 @@ public class MimeMessage extends Message {
}
}
@Override
public void writeHeaderTo(OutputStream out) throws IOException, MessagingException {
mHeader.writeTo(out);
}
@Override
public InputStream getInputStream() throws MessagingException {
return null;
@ -487,13 +494,11 @@ public class MimeMessage extends Message {
stack.addFirst(MimeMessage.this);
} else {
expect(Part.class);
try {
MimeMessage m = new MimeMessage();
((Part)stack.peek()).setBody(m);
stack.addFirst(m);
} catch (MessagingException me) {
throw new Error(me);
}
Part part = (Part) stack.peek();
MimeMessage m = new MimeMessage();
part.setBody(m);
stack.addFirst(m);
}
}
@ -519,7 +524,10 @@ public class MimeMessage extends Message {
Part e = (Part)stack.peek();
try {
MimeMultipart multiPart = new MimeMultipart(e.getContentType());
String contentType = e.getContentType();
String mimeType = MimeUtility.getHeaderParameter(contentType, null);
String boundary = MimeUtility.getHeaderParameter(contentType, "boundary");
MimeMultipart multiPart = new MimeMultipart(mimeType, boundary);
e.setBody(multiPart);
stack.addFirst(multiPart);
} catch (MessagingException me) {
@ -540,7 +548,21 @@ public class MimeMessage extends Message {
@Override
public void endMultipart() {
stack.removeFirst();
expect(Multipart.class);
Multipart multipart = (Multipart) stack.removeFirst();
boolean hasNoBodyParts = multipart.getCount() == 0;
boolean hasNoEpilogue = multipart.getEpilogue() == null;
if (hasNoBodyParts && hasNoEpilogue) {
/*
* The parser is calling startMultipart(), preamble(), and endMultipart() when all we have is
* headers of a "multipart/*" part. But there's really no point in keeping a Multipart body if all
* of the content is missing.
*/
expect(Part.class);
Part part = (Part) stack.peek();
part.setBody(null);
}
}
@Override
@ -686,4 +708,16 @@ public class MimeMessage extends Message {
setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
}
}
@Override
public String getServerExtra() {
return serverExtra;
}
@Override
public void setServerExtra(String serverExtra) {
this.serverExtra = serverExtra;
}
}

View File

@ -23,9 +23,10 @@ public class MimeMessageHelper {
if (body instanceof Multipart) {
Multipart multipart = ((Multipart) body);
multipart.setParent(part);
String type = multipart.getContentType();
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type);
if ("multipart/signed".equalsIgnoreCase(type)) {
String mimeType = multipart.getMimeType();
String contentType = String.format("%s; boundary=\"%s\"", mimeType, multipart.getBoundary());
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
if ("multipart/signed".equalsIgnoreCase(mimeType)) {
setEncoding(part, MimeUtil.ENC_7BIT);
} else {
setEncoding(part, MimeUtil.ENC_8BIT);

View File

@ -10,30 +10,26 @@ import java.util.Locale;
import java.util.Random;
public class MimeMultipart extends Multipart {
private byte[] mPreamble;
private byte[] mEpilogue;
private String mContentType;
private final String mBoundary;
private String mimeType;
private byte[] preamble;
private byte[] epilogue;
private final String boundary;
public MimeMultipart() throws MessagingException {
mBoundary = generateBoundary();
boundary = generateBoundary();
setSubType("mixed");
}
public MimeMultipart(String contentType) throws MessagingException {
this.mContentType = contentType;
try {
mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
if (mBoundary == null) {
throw new MessagingException("MultiPart does not contain boundary: " + contentType);
}
} catch (Exception e) {
throw new MessagingException(
"Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ contentType + ")", e);
public MimeMultipart(String mimeType, String boundary) throws MessagingException {
if (mimeType == null) {
throw new IllegalArgumentException("mimeType can't be null");
}
if (boundary == null) {
throw new IllegalArgumentException("boundary can't be null");
}
this.mimeType = mimeType;
this.boundary = boundary;
}
public String generateBoundary() {
@ -46,40 +42,53 @@ public class MimeMultipart extends Multipart {
return sb.toString().toUpperCase(Locale.US);
}
@Override
public String getBoundary() {
return boundary;
}
public byte[] getPreamble() {
return preamble;
}
public void setPreamble(byte[] preamble) {
this.mPreamble = preamble;
this.preamble = preamble;
}
public byte[] getEpilogue() {
return epilogue;
}
public void setEpilogue(byte[] epilogue) {
mEpilogue = epilogue;
this.epilogue = epilogue;
}
@Override
public String getContentType() {
return mContentType;
public String getMimeType() {
return mimeType;
}
public void setSubType(String subType) {
mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
mimeType = "multipart/" + subType;
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
if (mPreamble != null) {
out.write(mPreamble);
if (preamble != null) {
out.write(preamble);
writer.write("\r\n");
}
if (getBodyParts().isEmpty()) {
writer.write("--");
writer.write(mBoundary);
writer.write(boundary);
writer.write("\r\n");
} else {
for (BodyPart bodyPart : getBodyParts()) {
writer.write("--");
writer.write(mBoundary);
writer.write(boundary);
writer.write("\r\n");
writer.flush();
bodyPart.writeTo(out);
@ -88,11 +97,11 @@ public class MimeMultipart extends Multipart {
}
writer.write("--");
writer.write(mBoundary);
writer.write(boundary);
writer.write("--\r\n");
writer.flush();
if (mEpilogue != null) {
out.write(mEpilogue);
if (epilogue != null) {
out.write(epilogue);
}
}

View File

@ -16,10 +16,7 @@ import org.apache.james.mime4j.util.MimeUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
@ -1029,7 +1026,7 @@ public class MimeUtility {
@Override
public void close() throws IOException {
super.close();
rawInputStream.close();
closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream);
}
};
} else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) {
@ -1037,7 +1034,7 @@ public class MimeUtility {
@Override
public void close() throws IOException {
super.close();
rawInputStream.close();
closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream);
}
};
} else {
@ -1050,6 +1047,14 @@ public class MimeUtility {
return inputStream;
}
public static void closeInputStreamWithoutDeletingTemporaryFiles(InputStream rawInputStream) throws IOException {
if (rawInputStream instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) {
((BinaryTempFileBody.BinaryTempFileBodyInputStream) rawInputStream).closeWithoutDeleting();
} else {
rawInputStream.close();
}
}
public static String getMimeTypeByExtension(String filename) {
String returnedType = null;
String extension = null;

View File

@ -0,0 +1,6 @@
package com.fsck.k9.mail.internet;
public interface SizeAware {
long getSize();
}

View File

@ -10,10 +10,11 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import com.fsck.k9.mail.filter.CountingOutputStream;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil;
public class TextBody implements Body {
public class TextBody implements Body, SizeAware {
/**
* Immutable empty byte array
@ -98,4 +99,33 @@ public class TextBody implements Body {
public void setComposedMessageOffset(Integer composedMessageOffset) {
this.mComposedMessageOffset = composedMessageOffset;
}
@Override
public long getSize() {
try {
byte[] bytes = mBody.getBytes(mCharset);
if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) {
return bytes.length;
} else {
return getLengthWhenQuotedPrintableEncoded(bytes);
}
} catch (IOException e) {
throw new RuntimeException("Couldn't get body size", e);
}
}
private long getLengthWhenQuotedPrintableEncoded(byte[] bytes) throws IOException {
CountingOutputStream countingOutputStream = new CountingOutputStream();
OutputStream quotedPrintableOutputStream = new QuotedPrintableOutputStream(countingOutputStream, false);
try {
quotedPrintableOutputStream.write(bytes);
} finally {
try {
quotedPrintableOutputStream.close();
} catch (IOException e) { /* ignore */ }
}
return countingOutputStream.getCount();
}
}

View File

@ -0,0 +1,118 @@
package com.fsck.k9.mail.message;
import java.io.IOException;
import java.io.InputStream;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.parser.ContentHandler;
import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
public class MessageHeaderParser {
public static void parse(final Part part, InputStream headerInputStream) throws MessagingException {
MimeStreamParser parser = getMimeStreamParser();
parser.setContentHandler(new MessageHeaderParserContentHandler(part));
try {
parser.parse(headerInputStream);
} catch (MimeException me) {
throw new MessagingException("Error parsing headers", me);
} catch (IOException e) {
throw new MessagingException("I/O error parsing headers", e);
}
}
private static MimeStreamParser getMimeStreamParser() {
MimeConfig parserConfig = new MimeConfig();
parserConfig.setMaxHeaderLen(-1);
parserConfig.setMaxLineLen(-1);
parserConfig.setMaxHeaderCount(-1);
return new MimeStreamParser(parserConfig);
}
private static class MessageHeaderParserContentHandler implements ContentHandler {
private final Part part;
public MessageHeaderParserContentHandler(Part part) {
this.part = part;
}
@Override
public void field(Field rawField) throws MimeException {
String name = rawField.getName();
String raw = rawField.getRaw().toString();
try {
part.addRawHeader(name, raw);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
@Override
public void startMessage() throws MimeException {
/* do nothing */
}
@Override
public void endMessage() throws MimeException {
/* do nothing */
}
@Override
public void startBodyPart() throws MimeException {
/* do nothing */
}
@Override
public void endBodyPart() throws MimeException {
/* do nothing */
}
@Override
public void startHeader() throws MimeException {
/* do nothing */
}
@Override
public void endHeader() throws MimeException {
/* do nothing */
}
@Override
public void preamble(InputStream is) throws MimeException, IOException {
/* do nothing */
}
@Override
public void epilogue(InputStream is) throws MimeException, IOException {
/* do nothing */
}
@Override
public void startMultipart(BodyDescriptor bd) throws MimeException {
/* do nothing */
}
@Override
public void endMultipart() throws MimeException {
/* do nothing */
}
@Override
public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
/* do nothing */
}
@Override
public void raw(InputStream is) throws MimeException, IOException {
/* do nothing */
}
}
}

View File

@ -1456,13 +1456,9 @@ public class ImapStore extends RemoteStore {
throws MessagingException {
checkOpen(); //only need READ access
String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
if (parts == null) {
return;
}
String partId = part.getServerExtra();
String fetch;
String partId = parts[0];
if ("TEXT".equalsIgnoreCase(partId)) {
fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>",
mStoreConfig.getMaximumAutoDownloadMessageSize());
@ -1715,7 +1711,7 @@ public class ImapStore extends RemoteStore {
break;
}
}
part.setBody(mp);
MimeMessageHelper.setBody(part, mp);
} else {
/*
* This is a body. We need to add as much information as we can find out about
@ -1835,7 +1831,7 @@ public class ImapStore extends RemoteStore {
if (part instanceof ImapMessage) {
((ImapMessage) part).setSize(size);
}
part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
part.setServerExtra(id);
}
}

View File

@ -20,6 +20,7 @@ dependencies {
compile 'com.android.support:support-v13:21.0.2'
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10'
compile 'de.cketti.library.changelog:ckchangelog:1.2.1'
compile 'com.github.bumptech.glide:glide:3.4.0'
androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'

View File

@ -0,0 +1,78 @@
package com.fsck.k9.crypto;
import java.util.List;
import android.support.test.runner.AndroidJUnit4;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeBodyPart;
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.TextBody;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertSame;
@RunWith(AndroidJUnit4.class)
public class MessageDecryptVerifierTest {
@Test
public void findEncryptedPartsShouldReturnEmptyListForEmptyMessage() throws Exception {
MimeMessage emptyMessage = new MimeMessage();
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(emptyMessage);
assertEquals(0, encryptedParts.size());
}
@Test
public void findEncryptedPartsShouldReturnEmptyListForSimpleMessage() throws Exception {
MimeMessage message = new MimeMessage();
message.setBody(new TextBody("message text"));
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(0, encryptedParts.size());
}
@Test
public void findEncryptedPartsShouldReturnEmptyEncryptedPart() throws Exception {
MimeMessage message = new MimeMessage();
MimeMultipart mulitpartEncrypted = new MimeMultipart();
mulitpartEncrypted.setSubType("encrypted");
MimeMessageHelper.setBody(message, mulitpartEncrypted);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(1, encryptedParts.size());
assertSame(message, encryptedParts.get(0));
}
@Test
public void findEncryptedPartsShouldReturnMultipleEncryptedParts() throws Exception {
MimeMessage message = new MimeMessage();
MimeMultipart multipartMixed = new MimeMultipart();
multipartMixed.setSubType("mixed");
MimeMessageHelper.setBody(message, multipartMixed);
MimeMultipart mulitpartEncryptedOne = new MimeMultipart();
mulitpartEncryptedOne.setSubType("encrypted");
MimeBodyPart bodyPartOne = new MimeBodyPart(mulitpartEncryptedOne);
multipartMixed.addBodyPart(bodyPartOne);
MimeBodyPart bodyPartTwo = new MimeBodyPart(null, "text/plain");
multipartMixed.addBodyPart(bodyPartTwo);
MimeMultipart mulitpartEncryptedThree = new MimeMultipart();
mulitpartEncryptedThree.setSubType("encrypted");
MimeBodyPart bodyPartThree = new MimeBodyPart(mulitpartEncryptedThree);
multipartMixed.addBodyPart(bodyPartThree);
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
assertEquals(2, encryptedParts.size());
assertSame(bodyPartOne, encryptedParts.get(0));
assertSame(bodyPartThree, encryptedParts.get(1));
}
}

View File

@ -1,6 +1,8 @@
package com.fsck.k9.mailstore;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
@ -11,11 +13,14 @@ import com.fsck.k9.activity.K9ActivityCommon;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeBodyPart;
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.TextBody;
import com.fsck.k9.mail.internet.Viewable;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -38,7 +43,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, body);
// Extract text
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
List<Part> attachments = new ArrayList<Part>();
List<Viewable> viewables = MessageExtractor.getViewables(message, attachments);
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
viewables, attachments);
String expectedText = bodyText;
String expectedHtml =
@ -63,7 +71,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, body);
// Extract text
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
List<Part> attachments = new ArrayList<Part>();
List<Viewable> viewables = MessageExtractor.getViewables(message, attachments);
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
viewables, attachments);
String expectedText = "K-9 Mail rocks :>";
String expectedHtml =
@ -94,7 +105,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, multipart);
// Extract text
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
List<Part> attachments = new ArrayList<Part>();
List<Viewable> viewables = MessageExtractor.getViewables(message, attachments);
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
viewables, attachments);
String expectedText =
bodyText1 + "\r\n\r\n" +
@ -151,7 +165,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, multipart);
// Extract text
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
List<Part> attachments = new ArrayList<Part>();
List<Viewable> viewables = MessageExtractor.getViewables(message, attachments);
ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
viewables, attachments);
String expectedText =
bodyText +

View File

@ -1,64 +0,0 @@
package com.fsck.k9.mailstore;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.fsck.k9.Account;
import com.fsck.k9.Preferences;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeMultipart;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class LocalMessageTest {
private LocalMessage message;
private Account account;
private Preferences preferences;
@Before
public void setUp() throws Exception {
Context targetContext = InstrumentationRegistry.getTargetContext();
preferences = Preferences.getPreferences(targetContext);
account = preferences.newAccount();
LocalStore store = LocalStore.getInstance(account, targetContext);
message = new LocalMessage(store, "uid", new LocalFolder(store, "test"));
}
@After
public void tearDown() throws Exception {
preferences.deleteAccount(account);
}
@Test
public void testGetDisplayTextWithPlainTextPart() throws Exception {
String textBodyText = "text body";
MimeMultipart multipart = new MimeMultipart();
MimeBodyPart bodyPart1 = new MimeBodyPart(new LocalTextBody(textBodyText, textBodyText), "text/plain");
multipart.addBodyPart(bodyPart1);
message.setBody(multipart);
assertEquals("text body", message.getTextForDisplay());
}
@Test
public void testGetDisplayTextWithHtmlPart() throws Exception {
String htmlBodyText = "html body";
String textBodyText = "text body";
MimeMultipart multipart = new MimeMultipart();
MimeBodyPart bodyPart1 = new MimeBodyPart(new LocalTextBody(htmlBodyText, htmlBodyText), "text/html");
MimeBodyPart bodyPart2 = new MimeBodyPart(new LocalTextBody(textBodyText, textBodyText), "text/plain");
multipart.addBodyPart(bodyPart1);
multipart.addBodyPart(bodyPart2);
message.setBody(multipart);
assertEquals("html body", message.getTextForDisplay());
}
}

View File

@ -0,0 +1,141 @@
package com.fsck.k9.mailstore;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class MessageInfoExtractorTest {
@Test
public void shouldExtractPreviewFromSinglePlainTextPart() throws MessagingException {
MimeMessage message = new MimeMessage();
message.addHeader("Content-Type", "text/plain");
TextBody body = new TextBody("Message text ");
message.setBody(body);
String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
assertEquals("Message text", preview);
}
@Test
public void shouldLimitPreviewTo512Characters() throws MessagingException {
MimeMessage message = new MimeMessage();
message.addHeader("Content-Type", "text/plain");
TextBody body = new TextBody("10--------20--------30--------40--------50--------" +
"60--------70--------80--------90--------100-------" +
"110-------120-------130-------140-------150-------" +
"160-------170-------180-------190-------200-------" +
"210-------220-------230-------240-------250-------" +
"260-------270-------280-------290-------300-------" +
"310-------320-------330-------340-------350-------" +
"360-------370-------380-------390-------400-------" +
"410-------420-------430-------440-------450-------" +
"460-------470-------480-------490-------500-------" +
"510-------520-------530-------540-------550-------" +
"560-------570-------580-------590-------600-------");
message.setBody(body);
String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
assertEquals(512, preview.length());
assertEquals('…', preview.charAt(511));
}
@Test
public void shouldExtractPreviewFromSingleHtmlPart() throws MessagingException {
MimeMessage message = new MimeMessage();
message.addHeader("Content-Type", "text/html");
TextBody body = new TextBody("<html><body><pre>Message text</pre></body></html>");
message.setBody(body);
String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
assertEquals("Message text", preview);
}
@Test
public void shouldExtractPreviewFromMultipartAlternative() throws MessagingException {
MimeMessage message = new MimeMessage();
message.addHeader("Content-Type", "multipart/alternative");
MimeMultipart multipart = new MimeMultipart();
multipart.setSubType("alternative");
message.setBody(multipart);
TextBody textBody = new TextBody("text");
MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain");
multipart.addBodyPart(textPart);
TextBody htmlBody = new TextBody("<html><body>html</body></html>");
MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html");
multipart.addBodyPart(htmlPart);
String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
assertEquals("text", preview);
}
@Test
public void shouldExtractPreviewFromMultipartMixed() throws MessagingException {
MimeMessage message = new MimeMessage();
message.addHeader("Content-Type", "multipart/mixed");
MimeMultipart multipart = new MimeMultipart();
multipart.setSubType("mixed");
message.setBody(multipart);
TextBody textBody = new TextBody("text");
MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain");
multipart.addBodyPart(textPart);
TextBody htmlBody = new TextBody("<html><body>html</body></html>");
MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html");
multipart.addBodyPart(htmlPart);
String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
assertEquals("text / html", preview);
}
@Test
public void shouldExtractPreviewFromMultipartMixedWithInnerMesssage() throws MessagingException {
MimeMessage message = new MimeMessage();
message.addHeader("Content-Type", "multipart/mixed");
MimeMultipart multipart = new MimeMultipart();
multipart.setSubType("mixed");
message.setBody(multipart);
TextBody textBody = new TextBody("text");
MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain");
multipart.addBodyPart(textPart);
MimeMessage innerMessage = new MimeMessage();
innerMessage.addHeader("Content-Type", "text/html");
innerMessage.addHeader("Subject", "inner message");
TextBody htmlBody = new TextBody("<html><body>ht&#109;l</body></html>");
innerMessage.setBody(htmlBody);
MimeBodyPart messagePart = new MimeBodyPart(innerMessage, "message/rfc822");
multipart.addBodyPart(messagePart);
String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
assertEquals("text / Includes message titled \"inner message\" containing: html", preview);
}
private Context getContext() {
return InstrumentationRegistry.getTargetContext();
}
}

View File

@ -0,0 +1,176 @@
package com.fsck.k9.mailstore;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import android.content.Context;
import android.test.ApplicationTestCase;
import android.test.RenamingDelegatingContext;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.internet.MimeMessage;
import org.apache.james.mime4j.util.MimeUtil;
public class ReconstructMessageFromDatabaseTest extends ApplicationTestCase<K9> {
public static final String MESSAGE_SOURCE = "From: from@example.com\r\n" +
"To: to@example.com\r\n" +
"Subject: Test Message \r\n" +
"Date: Thu, 13 Nov 2014 17:09:38 +0100\r\n" +
"Content-Type: multipart/mixed;\r\n" +
" boundary=\"----Boundary\"\r\n" +
"Content-Transfer-Encoding: 8bit\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"This is a multipart MIME message.\r\n" +
"------Boundary\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"Content-Transfer-Encoding: 8bit\r\n" +
"\r\n" +
"Testing.\r\n" +
"This is a text body with some greek characters.\r\n" +
"αβγδεζηθ\r\n" +
"End of test.\r\n" +
"\r\n" +
"------Boundary\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"\r\n" +
"VGhpcyBpcyBhIHRl\r\n" +
"c3QgbWVzc2FnZQ==\r\n" +
"\r\n" +
"------Boundary--\r\n" +
"Hi, I'm the epilogue";
private Account account;
public ReconstructMessageFromDatabaseTest() {
super(K9.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
RenamingDelegatingContext context = new RenamingDelegatingContext(getContext(), "db-test-");
setContext(context);
BinaryTempFileBody.setTempDirectory(context.getCacheDir());
createApplication();
createDummyAccount(context);
}
private void createDummyAccount(Context context) {
account = new DummyAccount(context);
}
public void testThatByteIdenticalCopyOfMessageIsReconstructed() throws IOException, MessagingException {
LocalFolder folder = createFolderInDatabase();
MimeMessage message = parseMessage();
saveMessageToDatabase(folder, message);
LocalMessage localMessage = readMessageFromDatabase(folder, message);
String reconstructedMessage = writeMessageToString(localMessage);
assertEquals(MESSAGE_SOURCE, reconstructedMessage);
}
public void testAddMissingPart() throws MessagingException, IOException {
LocalFolder folder = createFolderInDatabase();
MimeMessage message = new MimeMessage();
message.addHeader("To", "to@example.com");
message.addHeader("MIME-Version", "1.0");
message.addHeader("Content-Type", "text/plain");
message.setServerExtra("text");
saveMessageToDatabase(folder, message);
LocalMessage localMessage = readMessageFromDatabase(folder, message);
assertEquals("to@example.com", localMessage.getHeader("To")[0]);
assertEquals("text/plain", localMessage.getMimeType());
assertEquals("text", localMessage.getServerExtra());
assertNull(localMessage.getBody());
Body body = new BinaryMemoryBody("Test message body".getBytes(), MimeUtil.ENC_7BIT);
localMessage.setBody(body);
folder.addPartToMessage(localMessage, localMessage);
LocalMessage completeLocalMessage = readMessageFromDatabase(folder, message);
String reconstructedMessage = writeMessageToString(completeLocalMessage);
assertEquals("To: to@example.com\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"Test message body",
reconstructedMessage);
}
protected MimeMessage parseMessage() throws IOException, MessagingException {
InputStream messageInputStream = new ByteArrayInputStream(MESSAGE_SOURCE.getBytes());
try {
return new MimeMessage(messageInputStream, true);
} finally {
messageInputStream.close();
}
}
protected LocalFolder createFolderInDatabase() throws MessagingException {
LocalStore localStore = LocalStore.getInstance(account, getApplication());
LocalFolder inbox = localStore.getFolder("INBOX");
localStore.createFolders(Collections.singletonList(inbox), 10);
return inbox;
}
protected void saveMessageToDatabase(LocalFolder folder, MimeMessage message) throws MessagingException {
folder.appendMessages(Collections.singletonList(message));
}
protected LocalMessage readMessageFromDatabase(LocalFolder folder, MimeMessage message) throws MessagingException {
LocalMessage localMessage = folder.getMessage(message.getUid());
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
folder.fetch(Collections.singletonList(localMessage), fp, null);
folder.close();
return localMessage;
}
protected String writeMessageToString(LocalMessage localMessage) throws IOException, MessagingException {
ByteArrayOutputStream messageOutputStream = new ByteArrayOutputStream();
try {
localMessage.writeTo(messageOutputStream);
} finally {
messageOutputStream.close();
}
return new String(messageOutputStream.toByteArray());
}
static class DummyAccount extends Account {
protected DummyAccount(Context context) {
super(context);
}
}
}

View File

@ -415,5 +415,17 @@
android:authorities="${applicationId}.provider.email"
android:exported="false"/>
<provider
android:name=".provider.K9FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/allowed_file_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -461,7 +461,8 @@ public class Account implements BaseAccount, StoreConfig {
mIsSignatureBeforeQuotedText = prefs.getBoolean(mUuid + ".signatureBeforeQuotedText", false);
identities = loadIdentities(prefs);
mCryptoApp = prefs.getString(mUuid + ".cryptoApp", NO_OPENPGP_PROVIDER);
String cryptoApp = prefs.getString(mUuid + ".cryptoApp", NO_OPENPGP_PROVIDER);
setCryptoApp(cryptoApp);
mAllowRemoteSearch = prefs.getBoolean(mUuid + ".allowRemoteSearch", false);
mRemoteSearchFullText = prefs.getBoolean(mUuid + ".remoteSearchFullText", false);
mRemoteSearchNumResults = prefs.getInt(mUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS);
@ -1597,7 +1598,11 @@ public class Account implements BaseAccount, StoreConfig {
}
public void setCryptoApp(String cryptoApp) {
mCryptoApp = cryptoApp;
if (cryptoApp == null || cryptoApp.equals("apg")) {
mCryptoApp = NO_OPENPGP_PROVIDER;
} else {
mCryptoApp = cryptoApp;
}
}
public boolean allowRemoteSearch() {
@ -1641,13 +1646,16 @@ public class Account implements BaseAccount, StoreConfig {
}
public synchronized String getOpenPgpProvider() {
// return null if set to "APG" or "None"
if (getCryptoApp().equals("apg") || getCryptoApp().equals("")) {
if (!isOpenPgpProviderConfigured()) {
return null;
}
return getCryptoApp();
}
public synchronized boolean isOpenPgpProviderConfigured() {
return !NO_OPENPGP_PROVIDER.equals(getCryptoApp());
}
public synchronized NotificationSetting getNotificationSetting() {
return mNotificationSetting;
}

View File

@ -1283,7 +1283,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
new String[] {"HtmlCleaner", "http://htmlcleaner.sourceforge.net/"},
new String[] {"Android-PullToRefresh", "https://github.com/chrisbanes/Android-PullToRefresh"},
new String[] {"ckChangeLog", "https://github.com/cketti/ckChangeLog"},
new String[] {"HoloColorPicker", "https://github.com/LarsWerkman/HoloColorPicker"}
new String[] {"HoloColorPicker", "https://github.com/LarsWerkman/HoloColorPicker"},
new String[] {"Glide", "https://github.com/bumptech/glide"}
};
private void onAbout() {

View File

@ -16,7 +16,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -43,7 +42,6 @@ import android.os.Parcelable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.util.Rfc822Tokenizer;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
@ -89,7 +87,7 @@ import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.ProgressDialogFragment;
import com.fsck.k9.helper.ContactItem;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.helper.SimpleTextWatcher;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.IdentityHelper;
import com.fsck.k9.helper.Utility;
@ -102,21 +100,19 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeBodyPart;
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.LocalAttachmentBody;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.TempFileBody;
import com.fsck.k9.mailstore.TempFileMessageBody;
import com.fsck.k9.message.IdentityField;
import com.fsck.k9.message.IdentityHeaderParser;
import com.fsck.k9.message.InsertableHtmlContent;
import com.fsck.k9.message.MessageBuilder;
import com.fsck.k9.message.QuotedTextMode;
import com.fsck.k9.message.SimpleMessageFormat;
import com.fsck.k9.ui.EolConvertingEditText;
import com.fsck.k9.view.MessageWebView;
import org.apache.james.mime4j.codec.EncoderUtil;
import org.apache.james.mime4j.util.MimeUtil;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
@ -264,12 +260,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
private Action mAction;
private enum QuotedTextMode {
NONE,
SHOW,
HIDE
}
private boolean mReadReceipt = false;
private QuotedTextMode mQuotedTextMode = QuotedTextMode.NONE;
@ -298,20 +288,14 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private Button mQuotedTextShow;
private View mQuotedTextBar;
private ImageButton mQuotedTextEdit;
private ImageButton mQuotedTextDelete;
private EolConvertingEditText mQuotedText;
private MessageWebView mQuotedHTML;
private InsertableHtmlContent mQuotedHtmlContent; // Container for HTML reply as it's being built.
private View mEncryptLayout;
private CheckBox mCryptoSignatureCheckbox;
private CheckBox mEncryptCheckbox;
private TextView mCryptoSignatureUserId;
private TextView mCryptoSignatureUserIdRest;
private ImageButton mAddToFromContacts;
private ImageButton mAddCcFromContacts;
private ImageButton mAddBccFromContacts;
private PgpData mPgpData = null;
private String mOpenPgpProvider;
private OpenPgpServiceConnection mOpenPgpServiceConnection;
@ -322,11 +306,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private boolean mSourceProcessed = false;
enum SimpleMessageFormat {
TEXT,
HTML
}
/**
* The currently used message format.
*
@ -410,8 +389,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
};
private Listener mListener = new Listener();
private EmailAddressAdapter mAddressAdapter;
private Validator mAddressValidator;
private FontSizes mFontSizes = K9.getFontSizes();
private ContextThemeWrapper mThemeContext;
@ -462,7 +439,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
* Compose a new message as a reply to the given message. If replyAll is true the function
* is reply all instead of simply reply.
* @param context
* @param account
* @param message
* @param replyAll
* @param messageBody optional, for decrypted messages, null if it should be grabbed from the given message
@ -522,11 +498,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// theme the whole content according to the theme (except the action bar)
mThemeContext = new ContextThemeWrapper(this,
K9.getK9ThemeResourceId(K9.getK9ComposerTheme()));
View v = ((LayoutInflater) mThemeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).
inflate(R.layout.message_compose, null);
View v = LayoutInflater.from(mThemeContext).inflate(R.layout.message_compose, null);
TypedValue outValue = new TypedValue();
// background color needs to be forced
mThemeContext.getTheme().resolveAttribute(R.attr.messageViewHeaderBackgroundColor, outValue, true);
mThemeContext.getTheme().resolveAttribute(R.attr.messageViewBackgroundColor, outValue, true);
v.setBackgroundColor(outValue.data);
setContentView(v);
} else {
@ -566,8 +541,8 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mContacts = Contacts.getInstance(MessageCompose.this);
mAddressAdapter = new EmailAddressAdapter(mThemeContext);
mAddressValidator = new EmailAddressValidator();
EmailAddressAdapter mAddressAdapter = new EmailAddressAdapter(mThemeContext);
Validator mAddressValidator = new EmailAddressValidator();
mChooseIdentityButton = (Button) findViewById(R.id.identity);
mChooseIdentityButton.setOnClickListener(this);
@ -583,9 +558,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mSubjectView = (EditText) findViewById(R.id.subject);
mSubjectView.getInputExtras(true).putBoolean("allowEmoji", true);
mAddToFromContacts = (ImageButton) findViewById(R.id.add_to);
mAddCcFromContacts = (ImageButton) findViewById(R.id.add_cc);
mAddBccFromContacts = (ImageButton) findViewById(R.id.add_bcc);
ImageButton mAddToFromContacts = (ImageButton) findViewById(R.id.add_to);
ImageButton mAddCcFromContacts = (ImageButton) findViewById(R.id.add_cc);
ImageButton mAddBccFromContacts = (ImageButton) findViewById(R.id.add_bcc);
mCcWrapper = (LinearLayout) findViewById(R.id.cc_wrapper);
mBccWrapper = (LinearLayout) findViewById(R.id.bcc_wrapper);
@ -603,7 +579,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mQuotedTextShow = (Button)findViewById(R.id.quoted_text_show);
mQuotedTextBar = findViewById(R.id.quoted_text_bar);
mQuotedTextEdit = (ImageButton)findViewById(R.id.quoted_text_edit);
mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete);
ImageButton mQuotedTextDelete = (ImageButton) findViewById(R.id.quoted_text_delete);
mQuotedText = (EolConvertingEditText)findViewById(R.id.quoted_text);
mQuotedText.getInputExtras(true).putBoolean("allowEmoji", true);
@ -618,81 +594,34 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
});
TextWatcher watcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int before, int after) {
/* do nothing */
}
TextWatcher draftNeedsChangingTextWatcher = new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true;
}
@Override
public void afterTextChanged(android.text.Editable s) { /* do nothing */ }
};
// For watching changes to the To:, Cc:, and Bcc: fields for auto-encryption on a matching
// address.
TextWatcher recipientWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int before, int after) {
/* do nothing */
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true;
}
@Override
public void afterTextChanged(android.text.Editable s) {
/* do nothing */
}
};
TextWatcher sigwatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int before, int after) {
/* do nothing */
}
TextWatcher signTextWatcher = new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true;
mSignatureChanged = true;
}
@Override
public void afterTextChanged(android.text.Editable s) { /* do nothing */ }
};
mToView.addTextChangedListener(recipientWatcher);
mCcView.addTextChangedListener(recipientWatcher);
mBccView.addTextChangedListener(recipientWatcher);
mSubjectView.addTextChangedListener(watcher);
mToView.addTextChangedListener(draftNeedsChangingTextWatcher);
mCcView.addTextChangedListener(draftNeedsChangingTextWatcher);
mBccView.addTextChangedListener(draftNeedsChangingTextWatcher);
mSubjectView.addTextChangedListener(draftNeedsChangingTextWatcher);
mMessageContentView.addTextChangedListener(watcher);
mQuotedText.addTextChangedListener(watcher);
mMessageContentView.addTextChangedListener(draftNeedsChangingTextWatcher);
mQuotedText.addTextChangedListener(draftNeedsChangingTextWatcher);
/* Yes, there really are poeple who ship versions of android without a contact picker */
/* Yes, there really are people who ship versions of android without a contact picker */
if (mContacts.hasContactPicker()) {
mAddToFromContacts.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
doLaunchContactPicker(CONTACT_PICKER_TO);
}
});
mAddCcFromContacts.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
doLaunchContactPicker(CONTACT_PICKER_CC);
}
});
mAddBccFromContacts.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
doLaunchContactPicker(CONTACT_PICKER_BCC);
}
});
mAddToFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_TO));
mAddCcFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_CC));
mAddBccFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_BCC));
} else {
mAddToFromContacts.setVisibility(View.GONE);
mAddCcFromContacts.setVisibility(View.GONE);
@ -762,7 +691,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mSignatureView = lowerSignature;
upperSignature.setVisibility(View.GONE);
}
mSignatureView.addTextChangedListener(sigwatcher);
mSignatureView.addTextChangedListener(signTextWatcher);
if (!mIdentity.getSignatureUse()) {
mSignatureView.setVisibility(View.GONE);
@ -814,34 +743,31 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mMessageReference.flag = Flag.FORWARDED;
}
mEncryptLayout = findViewById(R.id.layout_encrypt);
mCryptoSignatureCheckbox = (CheckBox)findViewById(R.id.cb_crypto_signature);
mCryptoSignatureCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateMessageFormat();
}
});
mCryptoSignatureUserId = (TextView)findViewById(R.id.userId);
mCryptoSignatureUserIdRest = (TextView)findViewById(R.id.userIdRest);
mEncryptCheckbox = (CheckBox)findViewById(R.id.cb_encrypt);
mEncryptCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateMessageFormat();
}
});
if (mSourceMessageBody != null) {
// mSourceMessageBody is set to something when replying to and forwarding decrypted
// messages, so the sender probably wants the message to be encrypted.
mEncryptCheckbox.setChecked(true);
}
final View mEncryptLayout = findViewById(R.id.layout_encrypt);
initializeCrypto();
mOpenPgpProvider = mAccount.getOpenPgpProvider();
if (mOpenPgpProvider != null) {
if (isCryptoProviderEnabled()) {
mCryptoSignatureCheckbox = (CheckBox)findViewById(R.id.cb_crypto_signature);
final OnCheckedChangeListener updateListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateMessageFormat();
}
};
mCryptoSignatureCheckbox.setOnCheckedChangeListener(updateListener);
mCryptoSignatureUserId = (TextView)findViewById(R.id.userId);
mCryptoSignatureUserIdRest = (TextView)findViewById(R.id.userIdRest);
mEncryptCheckbox = (CheckBox)findViewById(R.id.cb_encrypt);
mEncryptCheckbox.setOnCheckedChangeListener(updateListener);
if (mSourceMessageBody != null) {
// mSourceMessageBody is set to something when replying to and forwarding decrypted
// messages, so the sender probably wants the message to be encrypted.
mEncryptCheckbox.setChecked(true);
}
// New OpenPGP Provider API
// bind to service
@ -1037,6 +963,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
* Fill the encrypt layout with the latest data about signature key and encryption keys.
*/
public void updateEncryptLayout() {
if (!isCryptoProviderEnabled()) {
return;
}
if (!mPgpData.hasSignatureKey()) {
mCryptoSignatureCheckbox.setText(R.string.btn_crypto_sign);
mCryptoSignatureCheckbox.setChecked(false);
@ -1093,16 +1023,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ArrayList<Attachment> attachments = new ArrayList<Attachment>();
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
View view = mAttachments.getChildAt(i);
Attachment attachment = (Attachment) view.getTag();
attachments.add(attachment);
}
outState.putInt(STATE_KEY_NUM_ATTACHMENTS_LOADING, mNumAttachmentsLoading);
outState.putString(STATE_KEY_WAITING_FOR_ATTACHMENTS, mWaitingForAttachments.name());
outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments);
outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, createAttachmentList());
outState.putBoolean(STATE_KEY_CC_SHOWN, mCcWrapper.getVisibility() == View.VISIBLE);
outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccWrapper.getVisibility() == View.VISIBLE);
outState.putSerializable(STATE_KEY_QUOTED_TEXT_MODE, mQuotedTextMode);
@ -1249,435 +1173,55 @@ public class MessageCompose extends K9Activity implements OnClickListener,
return Address.parseUnencoded(addresses.trim());
}
/*
* Build the Body that will contain the text of the message. We'll decide where to
* include it later. Draft messages are treated somewhat differently in that signatures are not
* appended and HTML separators between composed text and quoted text are not added.
* @param isDraft If we should build a message that will be saved as a draft (as opposed to sent).
*/
private TextBody buildText(boolean isDraft) {
return buildText(isDraft, mMessageFormat);
return createMessageBuilder(isDraft).buildText();
}
/**
* Build the {@link Body} that will contain the text of the message.
*
* <p>
* Draft messages are treated somewhat differently in that signatures are not appended and HTML
* separators between composed text and quoted text are not added.
* </p>
*
* @param isDraft
* If {@code true} we build a message that will be saved as a draft (as opposed to
* sent).
* @param messageFormat
* Specifies what type of message to build ({@code text/plain} vs. {@code text/html}).
*
* @return {@link TextBody} instance that contains the entered text and possibly the quoted
* original message.
*/
private TextBody buildText(boolean isDraft, SimpleMessageFormat messageFormat) {
String messageText = mMessageContentView.getCharacters();
TextBodyBuilder textBodyBuilder = new TextBodyBuilder(messageText);
/*
* Find out if we need to include the original message as quoted text.
*
* We include the quoted text in the body if the user didn't choose to
* hide it. We always include the quoted text when we're saving a draft.
* That's so the user is able to "un-hide" the quoted text if (s)he
* opens a saved draft.
*/
boolean includeQuotedText = (isDraft || mQuotedTextMode == QuotedTextMode.SHOW);
boolean isReplyAfterQuote = (mQuoteStyle == QuoteStyle.PREFIX && mAccount.isReplyAfterQuote());
textBodyBuilder.setIncludeQuotedText(false);
if (includeQuotedText) {
if (messageFormat == SimpleMessageFormat.HTML && mQuotedHtmlContent != null) {
textBodyBuilder.setIncludeQuotedText(true);
textBodyBuilder.setQuotedTextHtml(mQuotedHtmlContent);
textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote);
}
String quotedText = mQuotedText.getCharacters();
if (messageFormat == SimpleMessageFormat.TEXT && quotedText.length() > 0) {
textBodyBuilder.setIncludeQuotedText(true);
textBodyBuilder.setQuotedText(quotedText);
textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote);
}
}
textBodyBuilder.setInsertSeparator(!isDraft);
boolean useSignature = (!isDraft && mIdentity.getSignatureUse());
if (useSignature) {
textBodyBuilder.setAppendSignature(true);
textBodyBuilder.setSignature(mSignatureView.getCharacters());
textBodyBuilder.setSignatureBeforeQuotedText(mAccount.isSignatureBeforeQuotedText());
} else {
textBodyBuilder.setAppendSignature(false);
}
TextBody body;
if (messageFormat == SimpleMessageFormat.HTML) {
body = textBodyBuilder.buildTextHtml();
} else {
body = textBodyBuilder.buildTextPlain();
}
return body;
}
/**
* Build the final message to be sent (or saved). If there is another message quoted in this one, it will be baked
* into the final message here.
* @param isDraft Indicates if this message is a draft or not. Drafts do not have signatures
* appended and have some extra metadata baked into their header for use during thawing.
* @return Message to be sent.
* @throws MessagingException
*/
private MimeMessage createMessage(boolean isDraft) throws MessagingException {
MimeMessage message = new MimeMessage();
message.addSentDate(new Date(), K9.hideTimeZone());
Address from = new Address(mIdentity.getEmail(), mIdentity.getName());
message.setFrom(from);
message.setRecipients(RecipientType.TO, getAddresses(mToView));
message.setRecipients(RecipientType.CC, getAddresses(mCcView));
message.setRecipients(RecipientType.BCC, getAddresses(mBccView));
message.setSubject(mSubjectView.getText().toString());
if (mReadReceipt) {
message.setHeader("Disposition-Notification-To", from.toEncodedString());
message.setHeader("X-Confirm-Reading-To", from.toEncodedString());
message.setHeader("Return-Receipt-To", from.toEncodedString());
}
if (!K9.hideUserAgent()) {
message.setHeader("User-Agent", getString(R.string.message_header_mua));
}
final String replyTo = mIdentity.getReplyTo();
if (replyTo != null) {
message.setReplyTo(new Address[] { new Address(replyTo) });
}
if (mInReplyTo != null) {
message.setInReplyTo(mInReplyTo);
}
if (mReferences != null) {
message.setReferences(mReferences);
}
// Build the body.
// TODO FIXME - body can be either an HTML or Text part, depending on whether we're in
// HTML mode or not. Should probably fix this so we don't mix up html and text parts.
TextBody body = null;
if (mPgpData.getEncryptedData() != null) {
String text = mPgpData.getEncryptedData();
body = new TextBody(text);
} else {
body = buildText(isDraft);
}
// text/plain part when mMessageFormat == MessageFormat.HTML
TextBody bodyPlain = null;
final boolean hasAttachments = mAttachments.getChildCount() > 0;
if (mMessageFormat == SimpleMessageFormat.HTML) {
// HTML message (with alternative text part)
// This is the compiled MIME part for an HTML message.
MimeMultipart composedMimeMessage = new MimeMultipart();
composedMimeMessage.setSubType("alternative"); // Let the receiver select either the text or the HTML part.
composedMimeMessage.addBodyPart(new MimeBodyPart(body, "text/html"));
bodyPlain = buildText(isDraft, SimpleMessageFormat.TEXT);
composedMimeMessage.addBodyPart(new MimeBodyPart(bodyPlain, "text/plain"));
if (hasAttachments) {
// If we're HTML and have attachments, we have a MimeMultipart container to hold the
// whole message (mp here), of which one part is a MimeMultipart container
// (composedMimeMessage) with the user's composed messages, and subsequent parts for
// the attachments.
MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(composedMimeMessage));
addAttachmentsToMessage(mp);
MimeMessageHelper.setBody(message, mp);
} else {
// If no attachments, our multipart/alternative part is the only one we need.
MimeMessageHelper.setBody(message, composedMimeMessage);
}
} else if (mMessageFormat == SimpleMessageFormat.TEXT) {
// Text-only message.
if (hasAttachments) {
MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(body, "text/plain"));
addAttachmentsToMessage(mp);
MimeMessageHelper.setBody(message, mp);
} else {
// No attachments to include, just stick the text body in the message and call it good.
MimeMessageHelper.setBody(message, body);
}
}
// If this is a draft, add metadata for thawing.
if (isDraft) {
// Add the identity to the message.
message.addHeader(K9.IDENTITY_HEADER, buildIdentityHeader(body, bodyPlain));
}
return message;
private MimeMessage createDraftMessage() throws MessagingException {
return createMessageBuilder(true).build();
}
/**
* Add attachments as parts into a MimeMultipart container.
* @param mp MimeMultipart container in which to insert parts.
* @throws MessagingException
*/
private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException {
Body body;
private MimeMessage createMessage() throws MessagingException {
return createMessageBuilder(false).build();
}
private MessageBuilder createMessageBuilder(boolean isDraft) {
return new MessageBuilder(getApplicationContext())
.setSubject(mSubjectView.getText().toString())
.setTo(getAddresses(mToView))
.setCc(getAddresses(mCcView))
.setBcc(getAddresses(mBccView))
.setInReplyTo(mInReplyTo)
.setReferences(mReferences)
.setRequestReadReceipt(mReadReceipt)
.setIdentity(mIdentity)
.setMessageFormat(mMessageFormat)
.setText(mMessageContentView.getCharacters())
.setPgpData(mPgpData)
.setAttachments(createAttachmentList())
.setSignature(mSignatureView.getCharacters())
.setQuoteStyle(mQuoteStyle)
.setQuotedTextMode(mQuotedTextMode)
.setQuotedText(mQuotedText.getCharacters())
.setQuotedHtmlContent(mQuotedHtmlContent)
.setReplyAfterQuote(mAccount.isReplyAfterQuote())
.setSignatureBeforeQuotedText(mAccount.isSignatureBeforeQuotedText())
.setIdentityChanged(mIdentityChanged)
.setSignatureChanged(mSignatureChanged)
.setCursorPosition(mMessageContentView.getSelectionStart())
.setMessageReference(mMessageReference)
.setDraft(isDraft);
}
private ArrayList<Attachment> createAttachmentList() {
ArrayList<Attachment> attachments = new ArrayList<Attachment>();
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
if (attachment.state != Attachment.LoadingState.COMPLETE) {
continue;
}
String contentType = attachment.contentType;
if (MimeUtil.isMessage(contentType)) {
body = new TempFileMessageBody(attachment.filename);
} else {
body = new TempFileBody(attachment.filename);
}
MimeBodyPart bp = new MimeBodyPart(body);
/*
* Correctly encode the filename here. Otherwise the whole
* header value (all parameters at once) will be encoded by
* MimeHeader.writeTo().
*/
bp.addHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n name=\"%s\"",
contentType,
EncoderUtil.encodeIfNecessary(attachment.name,
EncoderUtil.Usage.WORD_ENTITY, 7)));
bp.setEncoding(MimeUtility.getEncodingforType(contentType));
/*
* TODO: Oh the joys of MIME...
*
* From RFC 2183 (The Content-Disposition Header Field):
* "Parameter values longer than 78 characters, or which
* contain non-ASCII characters, MUST be encoded as specified
* in [RFC 2184]."
*
* Example:
*
* Content-Type: application/x-stuff
* title*1*=us-ascii'en'This%20is%20even%20more%20
* title*2*=%2A%2A%2Afun%2A%2A%2A%20
* title*3="isn't it!"
*/
bp.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, String.format(Locale.US,
"attachment;\r\n filename=\"%s\";\r\n size=%d",
attachment.name, attachment.size));
mp.addBodyPart(bp);
}
}
// FYI, there's nothing in the code that requires these variables to one letter. They're one
// letter simply to save space. This name sucks. It's too similar to Account.Identity.
private enum IdentityField {
LENGTH("l"),
OFFSET("o"),
FOOTER_OFFSET("fo"),
PLAIN_LENGTH("pl"),
PLAIN_OFFSET("po"),
MESSAGE_FORMAT("f"),
MESSAGE_READ_RECEIPT("r"),
SIGNATURE("s"),
NAME("n"),
EMAIL("e"),
// TODO - store a reference to the message being replied so we can mark it at the time of send.
ORIGINAL_MESSAGE("m"),
CURSOR_POSITION("p"), // Where in the message your cursor was when you saved.
QUOTED_TEXT_MODE("q"),
QUOTE_STYLE("qs");
private final String value;
IdentityField(String value) {
this.value = value;
View view = mAttachments.getChildAt(i);
Attachment attachment = (Attachment) view.getTag();
attachments.add(attachment);
}
public String value() {
return value;
}
/**
* Get the list of IdentityFields that should be integer values.
*
* <p>
* These values are sanity checked for integer-ness during decoding.
* </p>
*
* @return The list of integer {@link IdentityField}s.
*/
public static IdentityField[] getIntegerFields() {
return new IdentityField[] { LENGTH, OFFSET, FOOTER_OFFSET, PLAIN_LENGTH, PLAIN_OFFSET };
}
}
// Version identifier for "new style" identity. ! is an impossible value in base64 encoding, so we
// use that to determine which version we're in.
private static final String IDENTITY_VERSION_1 = "!";
/**
* Build the identity header string. This string contains metadata about a draft message to be
* used upon loading a draft for composition. This should be generated at the time of saving a
* draft.<br>
* <br>
* This is a URL-encoded key/value pair string. The list of possible values are in {@link IdentityField}.
* @param body {@link TextBody} to analyze for body length and offset.
* @param bodyPlain {@link TextBody} to analyze for body length and offset. May be null.
* @return Identity string.
*/
private String buildIdentityHeader(final TextBody body, final TextBody bodyPlain) {
Uri.Builder uri = new Uri.Builder();
if (body.getComposedMessageLength() != null && body.getComposedMessageOffset() != null) {
// See if the message body length is already in the TextBody.
uri.appendQueryParameter(IdentityField.LENGTH.value(), body.getComposedMessageLength().toString());
uri.appendQueryParameter(IdentityField.OFFSET.value(), body.getComposedMessageOffset().toString());
} else {
// If not, calculate it now.
uri.appendQueryParameter(IdentityField.LENGTH.value(), Integer.toString(body.getText().length()));
uri.appendQueryParameter(IdentityField.OFFSET.value(), Integer.toString(0));
}
if (mQuotedHtmlContent != null) {
uri.appendQueryParameter(IdentityField.FOOTER_OFFSET.value(),
Integer.toString(mQuotedHtmlContent.getFooterInsertionPoint()));
}
if (bodyPlain != null) {
if (bodyPlain.getComposedMessageLength() != null && bodyPlain.getComposedMessageOffset() != null) {
// See if the message body length is already in the TextBody.
uri.appendQueryParameter(IdentityField.PLAIN_LENGTH.value(), bodyPlain.getComposedMessageLength().toString());
uri.appendQueryParameter(IdentityField.PLAIN_OFFSET.value(), bodyPlain.getComposedMessageOffset().toString());
} else {
// If not, calculate it now.
uri.appendQueryParameter(IdentityField.PLAIN_LENGTH.value(), Integer.toString(body.getText().length()));
uri.appendQueryParameter(IdentityField.PLAIN_OFFSET.value(), Integer.toString(0));
}
}
// Save the quote style (useful for forwards).
uri.appendQueryParameter(IdentityField.QUOTE_STYLE.value(), mQuoteStyle.name());
// Save the message format for this offset.
uri.appendQueryParameter(IdentityField.MESSAGE_FORMAT.value(), mMessageFormat.name());
// If we're not using the standard identity of signature, append it on to the identity blob.
if (mIdentity.getSignatureUse() && mSignatureChanged) {
uri.appendQueryParameter(IdentityField.SIGNATURE.value(), mSignatureView.getCharacters());
}
if (mIdentityChanged) {
uri.appendQueryParameter(IdentityField.NAME.value(), mIdentity.getName());
uri.appendQueryParameter(IdentityField.EMAIL.value(), mIdentity.getEmail());
}
if (mMessageReference != null) {
uri.appendQueryParameter(IdentityField.ORIGINAL_MESSAGE.value(), mMessageReference.toIdentityString());
}
uri.appendQueryParameter(IdentityField.CURSOR_POSITION.value(), Integer.toString(mMessageContentView.getSelectionStart()));
uri.appendQueryParameter(IdentityField.QUOTED_TEXT_MODE.value(), mQuotedTextMode.name());
String k9identity = IDENTITY_VERSION_1 + uri.build().getEncodedQuery();
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Generated identity: " + k9identity);
}
return k9identity;
}
/**
* Parse an identity string. Handles both legacy and new (!) style identities.
*
* @param identityString
* The encoded identity string that was saved in a drafts header.
*
* @return A map containing the value for each {@link IdentityField} in the identity string.
*/
private Map<IdentityField, String> parseIdentityHeader(final String identityString) {
Map<IdentityField, String> identity = new HashMap<IdentityField, String>();
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Decoding identity: " + identityString);
}
if (identityString == null || identityString.length() < 1) {
return identity;
}
// Check to see if this is a "next gen" identity.
if (identityString.charAt(0) == IDENTITY_VERSION_1.charAt(0) && identityString.length() > 2) {
Uri.Builder builder = new Uri.Builder();
builder.encodedQuery(identityString.substring(1)); // Need to cut off the ! at the beginning.
Uri uri = builder.build();
for (IdentityField key : IdentityField.values()) {
String value = uri.getQueryParameter(key.value());
if (value != null) {
identity.put(key, value);
}
}
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Decoded identity: " + identity.toString());
}
// Sanity check our Integers so that recipients of this result don't have to.
for (IdentityField key : IdentityField.getIntegerFields()) {
if (identity.get(key) != null) {
try {
Integer.parseInt(identity.get(key));
} catch (NumberFormatException e) {
Log.e(K9.LOG_TAG, "Invalid " + key.name() + " field in identity: " + identity.get(key));
}
}
}
} else {
// Legacy identity
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Got a saved legacy identity: " + identityString);
}
StringTokenizer tokenizer = new StringTokenizer(identityString, ":", false);
// First item is the body length. We use this to separate the composed reply from the quoted text.
if (tokenizer.hasMoreTokens()) {
String bodyLengthS = Base64.decode(tokenizer.nextToken());
try {
identity.put(IdentityField.LENGTH, Integer.valueOf(bodyLengthS).toString());
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Unable to parse bodyLength '" + bodyLengthS + "'");
}
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.SIGNATURE, Base64.decode(tokenizer.nextToken()));
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.NAME, Base64.decode(tokenizer.nextToken()));
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.EMAIL, Base64.decode(tokenizer.nextToken()));
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.QUOTED_TEXT_MODE, Base64.decode(tokenizer.nextToken()));
}
}
return identity;
return attachments;
}
private void sendMessage() {
@ -1690,7 +1234,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private void saveIfNeeded() {
if (!mDraftNeedsSaving || mPreventDraftSaving || mPgpData.hasEncryptionKeys() ||
mEncryptCheckbox.isChecked() || !mAccount.hasDraftsFolder()) {
shouldEncrypt() || !mAccount.hasDraftsFolder()) {
return;
}
@ -1734,7 +1278,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
private void performSend() {
if (mOpenPgpProvider != null) {
if (isCryptoProviderEnabled()) {
// OpenPGP Provider API
// If not already encrypted but user wants to encrypt...
@ -1867,7 +1411,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
Log.e(K9.LOG_TAG, "OpenPGP Error Message:" + error.getMessage());
Toast.makeText(MessageCompose.this,
getString(R.string.openpgp_error) + " " + error.getMessage(),
getString(R.string.openpgp_error, error.getMessage()),
Toast.LENGTH_LONG).show();
}
});
@ -1946,7 +1490,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
@SuppressLint("InlinedApi")
private void onAddAttachment2(final String mime_type) {
if (mAccount.getOpenPgpProvider() != null) {
if (isCryptoProviderEnabled()) {
Toast.makeText(this, R.string.attachment_encryption_unsupported, Toast.LENGTH_LONG).show();
}
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
@ -2235,11 +1779,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
}
public void doLaunchContactPicker(int resultId) {
mIgnoreOnPause = true;
startActivityForResult(mContacts.contactPickerIntent(), resultId);
}
private void onAccountChosen(Account account, Identity identity) {
if (!mAccount.equals(account)) {
if (K9.DEBUG) {
@ -2411,7 +1950,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
onSend();
break;
case R.id.save:
if (mEncryptCheckbox.isChecked()) {
if (shouldEncrypt()) {
showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED);
} else {
onSave();
@ -2461,7 +2000,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override
public void onBackPressed() {
if (mDraftNeedsSaving) {
if (mEncryptCheckbox.isChecked()) {
if (shouldEncrypt()) {
showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED);
} else if (!mAccount.hasDraftsFolder()) {
showDialog(DIALOG_CONFIRM_DISCARD_ON_BACK);
@ -2622,17 +2161,19 @@ public class MessageCompose extends K9Activity implements OnClickListener,
String name = MimeUtility.getHeaderParameter(contentType, "name");
if (name != null) {
Body body = part.getBody();
if (body instanceof LocalAttachmentBody) {
final Uri uri = ((LocalAttachmentBody) body).getContentUri();
mHandler.post(new Runnable() {
@Override
public void run() {
addAttachment(uri);
}
});
} else {
return false;
}
//FIXME
// if (body instanceof LocalAttachmentBody) {
// final Uri uri = ((LocalAttachmentBody) body).getContentUri();
// mHandler.post(new Runnable() {
// @Override
// public void run() {
// addAttachment(uri);
// }
// });
// } else {
// return false;
// }
return false;
}
return true;
}
@ -2841,7 +2382,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// See buildIdentityHeader(TextBody) for a detailed description of the composition of this blob.
Map<IdentityField, String> k9identity = new HashMap<IdentityField, String>();
if (message.getHeader(K9.IDENTITY_HEADER) != null && message.getHeader(K9.IDENTITY_HEADER).length > 0 && message.getHeader(K9.IDENTITY_HEADER)[0] != null) {
k9identity = parseIdentityHeader(message.getHeader(K9.IDENTITY_HEADER)[0]);
k9identity = IdentityHeaderParser.parse(message.getHeader(K9.IDENTITY_HEADER)[0]);
}
Identity newIdentity = new Identity();
@ -3400,7 +2941,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
@Override
public void loadMessageForViewFinished(Account account, String folder, String uid, Message message) {
public void loadMessageForViewFinished(Account account, String folder, String uid, LocalMessage message) {
if ((mMessageReference == null) || !mMessageReference.uid.equals(uid)) {
return;
}
@ -3554,7 +3095,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
MimeMessage message;
try {
message = createMessage(false); // isDraft = true
message = createMessage();
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", me);
throw new RuntimeException("Failed to create a new message for send or save.", me);
@ -3587,7 +3128,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
MimeMessage message;
try {
message = createMessage(true); // isDraft = true
message = createDraftMessage();
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", me);
throw new RuntimeException("Failed to create a new message for send or save.", me);
@ -3906,7 +3447,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// Right now we send a text/plain-only message when the quoted text was edited, no
// matter what the user selected for the message format.
messageFormat = SimpleMessageFormat.TEXT;
} else if (mEncryptCheckbox.isChecked() || mCryptoSignatureCheckbox.isChecked()) {
} else if (shouldEncrypt() || shouldSign()) {
// Right now we only support PGP inline which doesn't play well with HTML. So force
// plain text in those cases.
messageFormat = SimpleMessageFormat.TEXT;
@ -3952,34 +3493,30 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
}
/**
* An {@link EditText} extension with methods that convert line endings from
* {@code \r\n} to {@code \n} and back again when setting and getting text.
*
*/
public static class EolConvertingEditText extends EditText {
private boolean isCryptoProviderEnabled() {
return mOpenPgpProvider != null;
}
public EolConvertingEditText(Context context, AttributeSet attrs) {
super(context, attrs);
private boolean shouldEncrypt() {
return isCryptoProviderEnabled() && mEncryptCheckbox.isChecked();
}
private boolean shouldSign() {
return isCryptoProviderEnabled() && mCryptoSignatureCheckbox.isChecked();
}
class DoLaunchOnClickListener implements OnClickListener {
private final int resultId;
DoLaunchOnClickListener(int resultId) {
this.resultId = resultId;
}
/**
* Return the text the EolConvertingEditText is displaying.
*
* @return A string with any line endings converted to {@code \r\n}.
*/
public String getCharacters() {
return getText().toString().replace("\n", "\r\n");
}
/**
* Sets the string value of the EolConvertingEditText. Any line endings
* in the string will be converted to {@code \n}.
*
* @param text
*/
public void setCharacters(CharSequence text) {
setText(text.toString().replace("\r\n", "\n"));
@Override
public void onClick(View v) {
mIgnoreOnPause = true;
startActivityForResult(mContacts.contactPickerIntent(), resultId);
}
}
}

View File

@ -40,8 +40,8 @@ import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.MessageListFragment;
import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener;
import com.fsck.k9.fragment.MessageViewFragment;
import com.fsck.k9.fragment.MessageViewFragment.MessageViewFragmentListener;
import com.fsck.k9.ui.messageview.MessageViewFragment;
import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener;
import com.fsck.k9.mailstore.StorageManager;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.search.LocalSearch;
@ -51,7 +51,6 @@ import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.view.MessageHeader;
import com.fsck.k9.view.MessageOpenPgpView;
import com.fsck.k9.view.MessageTitleView;
import com.fsck.k9.view.ViewSwitcher;
import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener;
@ -88,6 +87,7 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
private static final int PREVIOUS = 1;
private static final int NEXT = 2;
public static void actionDisplaySearch(Context context, SearchSpecification search,
boolean noThreading, boolean newTask) {
actionDisplaySearch(context, search, noThreading, newTask, true);
@ -1562,13 +1562,8 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// handle OpenPGP results from PendingIntents in OpenPGP view
// must be handled in this main activity, because startIntentSenderForResult() does not support Fragments
MessageOpenPgpView openPgpView = (MessageOpenPgpView) findViewById(R.id.layout_decrypt_openpgp);
if (openPgpView != null && openPgpView.handleOnActivityResult(requestCode, resultCode, data)) {
return;
if (mMessageViewFragment != null) {
mMessageViewFragment.handleCryptoResult(requestCode, resultCode, data);
}
}
}

View File

@ -45,7 +45,7 @@ import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.StorageManager;
import com.fsck.k9.service.MailService;
import org.openintents.openpgp.util.OpenPgpListPreference;
import org.openintents.openpgp.util.OpenPgpAppPreference;
import org.openintents.openpgp.util.OpenPgpUtils;
@ -174,7 +174,7 @@ public class AccountSettings extends K9PreferenceActivity {
private ListPreference mIdleRefreshPeriod;
private ListPreference mMaxPushFolders;
private boolean mHasCrypto = false;
private OpenPgpListPreference mCryptoApp;
private OpenPgpAppPreference mCryptoApp;
private PreferenceScreen mSearchScreen;
private CheckBoxPreference mCloudSearchEnabled;
@ -687,7 +687,7 @@ public class AccountSettings extends K9PreferenceActivity {
mHasCrypto = OpenPgpUtils.isAvailable(this);
if (mHasCrypto) {
mCryptoApp = (OpenPgpListPreference) findPreference(PREFERENCE_CRYPTO_APP);
mCryptoApp = (OpenPgpAppPreference) findPreference(PREFERENCE_CRYPTO_APP);
mCryptoApp.setValue(String.valueOf(mAccount.getCryptoApp()));
mCryptoApp.setSummary(mCryptoApp.getEntry());

View File

@ -3115,64 +3115,39 @@ public class MessagingController implements Runnable {
});
}
/**
* Mark the provided message as read if not disabled by the account setting.
*
* @param account
* The account the message belongs to.
* @param message
* The message to mark as read. This {@link Message} instance will be modify by calling
* {@link Message#setFlag(Flag, boolean)} on it.
*
* @throws MessagingException
*
* @see Account#isMarkMessageAsReadOnView()
*/
private void markMessageAsReadOnView(Account account, Message message)
public LocalMessage loadMessage(Account account, String folderName, String uid) throws MessagingException {
LocalStore localStore = account.getLocalStore();
LocalFolder localFolder = localStore.getFolder(folderName);
localFolder.open(Folder.OPEN_MODE_RW);
LocalMessage message = localFolder.getMessage(uid);
if (message == null || message.getId() == 0) {
throw new IllegalArgumentException("Message not found: folder=" + folderName + ", uid=" + uid);
}
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localFolder.fetch(Collections.singletonList(message), fp, null);
localFolder.close();
markMessageAsReadOnView(account, message);
return message;
}
private void markMessageAsReadOnView(Account account, LocalMessage message)
throws MessagingException {
if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) {
List<Long> messageIds = Collections.singletonList(message.getId());
setFlag(account, messageIds, Flag.SEEN, true);
((LocalMessage) message).setFlagInternal(Flag.SEEN, true);
message.setFlagInternal(Flag.SEEN, true);
}
}
/**
* Attempts to load the attachment specified by part from the given account and message.
* @param account
* @param message
* @param part
* @param listener
*/
public void loadAttachment(
final Account account,
final Message message,
final Part part,
final Object tag,
final MessagingListener listener) {
/*
* Check if the attachment has already been downloaded. If it has there's no reason to
* download it, so we just tell the listener that it's ready to go.
*/
if (part.getBody() != null) {
for (MessagingListener l : getListeners(listener)) {
l.loadAttachmentStarted(account, message, part, tag, false);
}
for (MessagingListener l : getListeners(listener)) {
l.loadAttachmentFinished(account, message, part, tag);
}
return;
}
for (MessagingListener l : getListeners(listener)) {
l.loadAttachmentStarted(account, message, part, tag, true);
}
public void loadAttachment(final Account account, final LocalMessage message, final Part part,
final MessagingListener listener) {
put("loadAttachment", listener, new Runnable() {
@Override
@ -3180,32 +3155,29 @@ public class MessagingController implements Runnable {
Folder remoteFolder = null;
LocalFolder localFolder = null;
try {
LocalStore localStore = account.getLocalStore();
String folderName = message.getFolder().getName();
LocalStore localStore = account.getLocalStore();
localFolder = localStore.getFolder(folderName);
List<Part> attachments = MessageExtractor.collectAttachments(message);
for (Part attachment : attachments) {
attachment.setBody(null);
}
Store remoteStore = account.getRemoteStore();
localFolder = localStore.getFolder(message.getFolder().getName());
remoteFolder = remoteStore.getFolder(message.getFolder().getName());
remoteFolder = remoteStore.getFolder(folderName);
remoteFolder.open(Folder.OPEN_MODE_RW);
//FIXME: This is an ugly hack that won't be needed once the Message objects have been united.
Message remoteMessage = remoteFolder.getMessage(message.getUid());
MimeMessageHelper.setBody(remoteMessage, message.getBody());
remoteFolder.fetchPart(remoteMessage, part, null);
localFolder.updateMessage((LocalMessage)message);
localFolder.addPartToMessage(message, part);
for (MessagingListener l : getListeners(listener)) {
l.loadAttachmentFinished(account, message, part, tag);
l.loadAttachmentFinished(account, message, part);
}
} catch (MessagingException me) {
if (K9.DEBUG)
Log.v(K9.LOG_TAG, "Exception loading attachment", me);
for (MessagingListener l : getListeners(listener)) {
l.loadAttachmentFailed(account, message, part, tag, me.getMessage());
l.loadAttachmentFailed(account, message, part, me.getMessage());
}
notifyUserIfCertificateProblem(context, me, account, true);
addErrorMessage(account, null, me);
@ -4015,7 +3987,7 @@ public class MessagingController implements Runnable {
@Override
public void act(final Account account, final Folder folder,
final List<Message> accountMessages) {
final List<Message> accountMessages) {
suppressMessages(account, messages);
putBackground("deleteMessages", null, new Runnable() {

View File

@ -91,7 +91,7 @@ public class MessagingListener {
Message message) {}
public void loadMessageForViewFinished(Account account, String folder, String uid,
Message message) {}
LocalMessage message) {}
public void loadMessageForViewFailed(Account account, String folder, String uid,
Throwable t) {}
@ -133,13 +133,9 @@ public class MessagingListener {
public void setPushActive(Account account, String folderName, boolean enabled) {}
public void loadAttachmentStarted(Account account, Message message, Part part, Object tag,
boolean requiresDownload) {}
public void loadAttachmentFinished(Account account, Message message, Part part) {}
public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) {}
public void loadAttachmentFailed(Account account, Message message, Part part, Object tag,
String reason) {}
public void loadAttachmentFailed(Account account, Message message, Part part, String reason) {}

View File

@ -0,0 +1,70 @@
package com.fsck.k9.crypto;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.RawDataBody;
import com.fsck.k9.mail.internet.SizeAware;
import org.apache.commons.io.IOUtils;
public class DecryptedTempFileBody implements RawDataBody, SizeAware {
private final File tempDirectory;
private final String encoding;
private File file;
public DecryptedTempFileBody(String encoding, File tempDirectory) {
this.encoding = encoding;
this.tempDirectory = tempDirectory;
}
@Override
public String getEncoding() {
return encoding;
}
@Override
public void setEncoding(String encoding) throws MessagingException {
throw new RuntimeException("Not supported");
}
public OutputStream getOutputStream() throws IOException {
file = File.createTempFile("decrypted", null, tempDirectory);
return new FileOutputStream(file);
}
@Override
public InputStream getInputStream() throws MessagingException {
try {
return new FileInputStream(file);
} catch (IOException ioe) {
throw new MessagingException("Unable to open body", ioe);
}
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
InputStream in = getInputStream();
try {
IOUtils.copy(in, out);
} finally {
in.close();
}
}
@Override
public long getSize() {
return file.length();
}
public File getFile() {
return file;
}
}

View File

@ -0,0 +1,134 @@
package com.fsck.k9.crypto;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import org.openintents.openpgp.util.OpenPgpUtils;
public class MessageDecryptVerifier {
private static final String MULTIPART_ENCRYPTED = "multipart/encrypted";
private static final String MULTIPART_SIGNED = "multipart/signed";
private static final String PROTOCOL_PARAMETER = "protocol";
private static final String APPLICATION_PGP_ENCRYPTED = "application/pgp-encrypted";
private static final String APPLICATION_PGP_SIGNATURE = "application/pgp-signature";
private static final String TEXT_PLAIN = "text/plain";
public static List<Part> findEncryptedParts(Part startPart) {
List<Part> encryptedParts = new ArrayList<Part>();
Stack<Part> partsToCheck = new Stack<Part>();
partsToCheck.push(startPart);
while (!partsToCheck.isEmpty()) {
Part part = partsToCheck.pop();
String mimeType = part.getMimeType();
Body body = part.getBody();
if (MULTIPART_ENCRYPTED.equals(mimeType)) {
encryptedParts.add(part);
} else if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
for (int i = multipart.getCount() - 1; i >= 0; i--) {
BodyPart bodyPart = multipart.getBodyPart(i);
partsToCheck.push(bodyPart);
}
}
}
return encryptedParts;
}
public static List<Part> findSignedParts(Part startPart) {
List<Part> signedParts = new ArrayList<Part>();
Stack<Part> partsToCheck = new Stack<Part>();
partsToCheck.push(startPart);
while (!partsToCheck.isEmpty()) {
Part part = partsToCheck.pop();
String mimeType = part.getMimeType();
Body body = part.getBody();
if (MULTIPART_SIGNED.equals(mimeType)) {
signedParts.add(part);
} else if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
for (int i = multipart.getCount() - 1; i >= 0; i--) {
BodyPart bodyPart = multipart.getBodyPart(i);
partsToCheck.push(bodyPart);
}
}
}
return signedParts;
}
public static List<Part> findPgpInlineParts(Part startPart) {
List<Part> inlineParts = new ArrayList<Part>();
Stack<Part> partsToCheck = new Stack<Part>();
partsToCheck.push(startPart);
while (!partsToCheck.isEmpty()) {
Part part = partsToCheck.pop();
String mimeType = part.getMimeType();
Body body = part.getBody();
if (TEXT_PLAIN.equalsIgnoreCase(mimeType)) {
String text = MessageExtractor.getTextFromPart(part);
switch (OpenPgpUtils.parseMessage(text)) {
case OpenPgpUtils.PARSE_RESULT_MESSAGE:
case OpenPgpUtils.PARSE_RESULT_SIGNED_MESSAGE:
inlineParts.add(part);
}
} else if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
for (int i = multipart.getCount() - 1; i >= 0; i--) {
BodyPart bodyPart = multipart.getBodyPart(i);
partsToCheck.push(bodyPart);
}
}
}
return inlineParts;
}
public static byte[] getSignatureData(Part part) throws IOException, MessagingException {
if (MULTIPART_SIGNED.equals(part.getMimeType())) {
Body body = part.getBody();
if (body instanceof Multipart) {
Multipart multi = (Multipart) body;
BodyPart signatureBody = multi.getBodyPart(1);
if (APPLICATION_PGP_SIGNATURE.equals(signatureBody.getMimeType())) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
signatureBody.getBody().writeTo(bos);
return bos.toByteArray();
}
}
}
return null;
}
public static boolean isPgpMimeSignedPart(Part part) {
return MULTIPART_SIGNED.equals(part.getMimeType());
}
public static boolean isPgpMimeEncryptedPart(Part part) {
//FIXME: Doesn't work right now because LocalMessage.getContentType() doesn't load headers from database
// String contentType = part.getContentType();
// String protocol = MimeUtility.getHeaderParameter(contentType, PROTOCOL_PARAMETER);
// return APPLICATION_PGP_ENCRYPTED.equals(protocol);
return MULTIPART_ENCRYPTED.equals(part.getMimeType());
}
}

View File

@ -0,0 +1,22 @@
package com.fsck.k9.helper;
import android.text.Editable;
import android.text.TextWatcher;
/**
* all methods empty - but this way we can have TextWatchers with less boilder-plate where
* we just override the methods we want and not always al 3
*/
public class SimpleTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
}

View File

@ -393,7 +393,8 @@ public class Utility {
public static boolean hasExternalImages(final String message) {
Matcher imgMatches = IMG_PATTERN.matcher(message);
while (imgMatches.find()) {
if (!imgMatches.group(1).equals("content")) {
String uriScheme = imgMatches.group(1);
if (uriScheme.equals("http") || uriScheme.equals("https")) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "External images found");
}

View File

@ -0,0 +1,36 @@
package com.fsck.k9.mailstore;
import android.net.Uri;
import com.fsck.k9.mail.Part;
public class AttachmentViewInfo {
public static final long UNKNOWN_SIZE = -1;
public final String mimeType;
public final String displayName;
public final long size;
/**
* A content provider URI that can be used to retrieve the decoded attachment.
* <p/>
* Note: All content providers must support an alternative MIME type appended as last URI segment.
*
* @see com.fsck.k9.ui.messageview.AttachmentController#getAttachmentUriForMimeType(AttachmentViewInfo, String)
*/
public final Uri uri;
public final boolean firstClassAttachment;
public final Part part;
public AttachmentViewInfo(String mimeType, String displayName, long size, Uri uri, boolean firstClassAttachment,
Part part) {
this.mimeType = mimeType;
this.displayName = displayName;
this.size = size;
this.uri = uri;
this.firstClassAttachment = firstClassAttachment;
this.part = part;
}
}

View File

@ -0,0 +1,48 @@
package com.fsck.k9.mailstore;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.RawDataBody;
import com.fsck.k9.mail.internet.SizeAware;
public class BinaryMemoryBody implements Body, RawDataBody, SizeAware {
private final byte[] data;
private final String encoding;
public BinaryMemoryBody(byte[] data, String encoding) {
this.data = data;
this.encoding = encoding;
}
@Override
public String getEncoding() {
return encoding;
}
@Override
public InputStream getInputStream() throws MessagingException {
return new ByteArrayInputStream(data);
}
@Override
public void setEncoding(String encoding) throws UnavailableStorageException, MessagingException {
throw new RuntimeException("nope"); //FIXME
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
out.write(data);
}
@Override
public long getSize() {
return data.length;
}
}

View File

@ -0,0 +1,231 @@
package com.fsck.k9.mailstore;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Stack;
import android.content.Context;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.crypto.DecryptedTempFileBody;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.codec.Base64InputStream;
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
import org.apache.james.mime4j.io.EOLConvertingInputStream;
import org.apache.james.mime4j.parser.ContentHandler;
import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.util.MimeUtil;
// TODO rename this class? this class doesn't really bear any 'decrypted' semantics anymore...
public class DecryptStreamParser {
private static final String DECRYPTED_CACHE_DIRECTORY = "decrypted";
public static MimeBodyPart parse(Context context, InputStream inputStream) throws MessagingException, IOException {
File decryptedTempDirectory = getDecryptedTempDirectory(context);
MimeBodyPart decryptedRootPart = new MimeBodyPart();
MimeConfig parserConfig = new MimeConfig();
parserConfig.setMaxHeaderLen(-1);
parserConfig.setMaxLineLen(-1);
parserConfig.setMaxHeaderCount(-1);
MimeStreamParser parser = new MimeStreamParser(parserConfig);
parser.setContentHandler(new PartBuilder(decryptedTempDirectory, decryptedRootPart));
parser.setRecurse();
inputStream = new BufferedInputStream(inputStream, 4096);
try {
parser.parse(new EOLConvertingInputStream(inputStream));
} catch (MimeException e) {
throw new MessagingException("Failed to parse decrypted content", e);
}
return decryptedRootPart;
}
private static Body createBody(InputStream inputStream, String transferEncoding, File decryptedTempDirectory)
throws IOException {
DecryptedTempFileBody body = new DecryptedTempFileBody(transferEncoding, decryptedTempDirectory);
OutputStream outputStream = body.getOutputStream();
try {
InputStream decodingInputStream;
boolean closeStream;
if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(transferEncoding)) {
decodingInputStream = new QuotedPrintableInputStream(inputStream, false);
closeStream = true;
} else if (MimeUtil.ENC_BASE64.equals(transferEncoding)) {
decodingInputStream = new Base64InputStream(inputStream);
closeStream = true;
} else {
decodingInputStream = inputStream;
closeStream = false;
}
try {
IOUtils.copy(decodingInputStream, outputStream);
} finally {
if (closeStream) {
decodingInputStream.close();
}
}
} finally {
outputStream.close();
}
return body;
}
private static File getDecryptedTempDirectory(Context context) {
File directory = new File(context.getCacheDir(), DECRYPTED_CACHE_DIRECTORY);
if (!directory.exists()) {
if (!directory.mkdir()) {
Log.e(K9.LOG_TAG, "Error creating directory: " + directory.getAbsolutePath());
}
}
return directory;
}
private static class PartBuilder implements ContentHandler {
private final File decryptedTempDirectory;
private final MimeBodyPart decryptedRootPart;
private final Stack<Object> stack = new Stack<Object>();
public PartBuilder(File decryptedTempDirectory, MimeBodyPart decryptedRootPart)
throws MessagingException {
this.decryptedTempDirectory = decryptedTempDirectory;
this.decryptedRootPart = decryptedRootPart;
}
@Override
public void startMessage() throws MimeException {
if (stack.isEmpty()) {
stack.push(decryptedRootPart);
} else {
Part part = (Part) stack.peek();
Message innerMessage = new MimeMessage();
part.setBody(innerMessage);
stack.push(innerMessage);
}
}
@Override
public void endMessage() throws MimeException {
stack.pop();
}
@Override
public void startBodyPart() throws MimeException {
try {
Multipart multipart = (Multipart) stack.peek();
BodyPart bodyPart = new MimeBodyPart();
multipart.addBodyPart(bodyPart);
stack.push(bodyPart);
} catch (MessagingException e) {
throw new MimeException(e);
}
}
@Override
public void endBodyPart() throws MimeException {
stack.pop();
}
@Override
public void startHeader() throws MimeException {
// Do nothing
}
@Override
public void field(Field parsedField) throws MimeException {
try {
String name = parsedField.getName();
String raw = parsedField.getRaw().toString();
Part part = (Part) stack.peek();
part.addRawHeader(name, raw);
} catch (MessagingException e) {
throw new MimeException(e);
}
}
@Override
public void endHeader() throws MimeException {
// Do nothing
}
@Override
public void preamble(InputStream is) throws MimeException, IOException {
// Do nothing
}
@Override
public void epilogue(InputStream is) throws MimeException, IOException {
// Do nothing
}
@Override
public void startMultipart(BodyDescriptor bd) throws MimeException {
Part part = (Part) stack.peek();
try {
String contentType = part.getContentType();
String mimeType = MimeUtility.getHeaderParameter(contentType, null);
String boundary = MimeUtility.getHeaderParameter(contentType, "boundary");
MimeMultipart multipart = new MimeMultipart(mimeType, boundary);
part.setBody(multipart);
stack.push(multipart);
} catch (MessagingException e) {
throw new MimeException(e);
}
}
@Override
public void endMultipart() throws MimeException {
stack.pop();
}
@Override
public void body(BodyDescriptor bd, InputStream inputStream) throws MimeException, IOException {
Part part = (Part) stack.peek();
String transferEncoding = bd.getTransferEncoding();
Body body = createBody(inputStream, transferEncoding, decryptedTempDirectory);
part.setBody(body);
}
@Override
public void raw(InputStream is) throws MimeException, IOException {
throw new IllegalStateException("Not implemented");
}
}
}

View File

@ -0,0 +1,60 @@
package com.fsck.k9.mailstore;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.RawDataBody;
import com.fsck.k9.mail.internet.SizeAware;
import org.apache.commons.io.IOUtils;
public class FileBackedBody implements Body, SizeAware, RawDataBody {
private final File file;
private final String encoding;
public FileBackedBody(File file, String encoding) {
this.file = file;
this.encoding = encoding;
}
@Override
public InputStream getInputStream() throws MessagingException {
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new MessagingException("File not found", e);
}
}
@Override
public void setEncoding(String encoding) throws MessagingException {
throw new RuntimeException("not supported");
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
InputStream in = getInputStream();
try {
IOUtils.copy(in, out);
} finally {
in.close();
}
}
@Override
public long getSize() {
return file.length();
}
@Override
public String getEncoding() {
return encoding;
}
}

View File

@ -1,40 +0,0 @@
package com.fsck.k9.mailstore;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import android.content.Context;
import android.net.Uri;
import com.fsck.k9.mail.MessagingException;
/**
* An attachment whose contents are loaded from an URI.
*/
public class LocalAttachmentBody extends BinaryAttachmentBody {
private Context context;
private Uri mUri;
public LocalAttachmentBody(Uri uri, Context context) {
this.context = context;
mUri = uri;
}
@Override
public InputStream getInputStream() throws MessagingException {
try {
return context.getContentResolver().openInputStream(mUri);
} catch (FileNotFoundException fnfe) {
/*
* Since it's completely normal for us to try to serve up attachments that
* have been blown away, we just return an empty stream.
*/
return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY);
}
}
public Uri getContentUri() {
return mUri;
}
}

View File

@ -1,31 +0,0 @@
package com.fsck.k9.mailstore;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeBodyPart;
public class LocalAttachmentBodyPart extends MimeBodyPart {
private long mAttachmentId = -1;
public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException {
super(body);
mAttachmentId = attachmentId;
}
/**
* Returns the local attachment id of this body, or -1 if it is not stored.
* @return
*/
public long getAttachmentId() {
return mAttachmentId;
}
public void setAttachmentId(long attachmentId) {
mAttachmentId = attachmentId;
}
@Override
public String toString() {
return "" + mAttachmentId;
}
}

View File

@ -1,49 +0,0 @@
package com.fsck.k9.mailstore;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.james.mime4j.util.MimeUtil;
import android.content.Context;
import android.net.Uri;
import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException;
/**
* A {@link LocalAttachmentBody} extension containing a message/rfc822 type body
*
*/
class LocalAttachmentMessageBody extends LocalAttachmentBody implements CompositeBody {
public LocalAttachmentMessageBody(Uri uri, Context context) {
super(uri, context);
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
AttachmentMessageBodyUtil.writeTo(this, out);
}
@Override
public void setUsing7bitTransport() throws MessagingException {
/*
* There's nothing to recurse into here, so there's nothing to do.
* The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once
* writeTo() is called, the file with the rfc822 body will be opened
* for reading and will then be recursed.
*/
}
@Override
public void setEncoding(String encoding) throws MessagingException {
if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)
&& !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
throw new MessagingException(
"Incompatible content-transfer-encoding applied to a CompositeBody");
}
mEncoding = encoding;
}
}

View File

@ -0,0 +1,56 @@
package com.fsck.k9.mailstore;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeBodyPart;
public class LocalBodyPart extends MimeBodyPart implements LocalPart {
private final String accountUuid;
private final LocalMessage message;
private final long messagePartId;
private final String displayName;
private final long size;
private final boolean firstClassAttachment;
public LocalBodyPart(String accountUuid, LocalMessage message, long messagePartId, String displayName, long size,
boolean firstClassAttachment) throws MessagingException {
super();
this.accountUuid = accountUuid;
this.message = message;
this.messagePartId = messagePartId;
this.displayName = displayName;
this.size = size;
this.firstClassAttachment = firstClassAttachment;
}
@Override
public String getAccountUuid() {
return accountUuid;
}
@Override
public long getId() {
return messagePartId;
}
@Override
public String getDisplayName() {
return displayName;
}
@Override
public long getSize() {
return size;
}
@Override
public boolean isFirstClassAttachment() {
return firstClassAttachment;
}
@Override
public LocalMessage getMessage() {
return message;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,7 @@ package com.fsck.k9.mailstore;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import android.content.ContentValues;
@ -37,10 +35,11 @@ public class LocalMessage extends MimeMessage {
private String mPreview = "";
private boolean mHeadersLoaded = false;
private boolean mMessageDirty = false;
private long mThreadId;
private long mRootId;
private long messagePartId;
private String mimeType;
private LocalMessage(LocalStore localStore) {
this.localStore = localStore;
@ -111,30 +110,19 @@ public class LocalMessage extends MimeMessage {
setFlagInternal(Flag.FLAGGED, flagged);
setFlagInternal(Flag.ANSWERED, answered);
setFlagInternal(Flag.FORWARDED, forwarded);
messagePartId = cursor.getLong(22);
mimeType = cursor.getString(23);
}
/**
* Fetch the message text for display. This always returns an HTML-ified version of the
* message, even if it was originally a text-only message.
* @return HTML version of message for display purposes or null.
* @throws MessagingException
*/
public String getTextForDisplay() throws MessagingException {
String text = null; // First try and fetch an HTML part.
Part part = MimeUtility.findFirstPartByMimeType(this, "text/html");
if (part == null) {
// If that fails, try and get a text part.
part = MimeUtility.findFirstPartByMimeType(this, "text/plain");
if (part != null && part.getBody() instanceof LocalTextBody) {
text = ((LocalTextBody) part.getBody()).getBodyForDisplay();
}
} else {
// We successfully found an HTML part; do the necessary character set decoding.
text = MessageExtractor.getTextFromPart(part);
}
return text;
long getMessagePartId() {
return messagePartId;
}
@Override
public String getMimeType() {
return mimeType;
}
/* Custom version of writeTo that updates the MIME message based on localMessage
* changes.
@ -142,30 +130,13 @@ public class LocalMessage extends MimeMessage {
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
if (mMessageDirty) buildMimeRepresentation();
if (!mHeadersLoaded) {
loadHeaders();
}
super.writeTo(out);
}
void buildMimeRepresentation() throws MessagingException {
if (!mMessageDirty) {
return;
}
super.setSubject(mSubject);
if (this.mFrom != null && this.mFrom.length > 0) {
super.setFrom(this.mFrom[0]);
}
super.setReplyTo(mReplyTo);
super.setSentDate(this.getSentDate(), K9.hideTimeZone());
super.setRecipients(RecipientType.TO, mTo);
super.setRecipients(RecipientType.CC, mCc);
super.setRecipients(RecipientType.BCC, mBcc);
if (mMessageId != null) super.setMessageId(mMessageId);
mMessageDirty = false;
}
@Override
public String getPreview() {
return mPreview;
@ -180,14 +151,12 @@ public class LocalMessage extends MimeMessage {
@Override
public void setSubject(String subject) throws MessagingException {
mSubject = subject;
mMessageDirty = true;
}
@Override
public void setMessageId(String messageId) {
mMessageId = messageId;
mMessageDirty = true;
}
@Override
@ -208,7 +177,6 @@ public class LocalMessage extends MimeMessage {
@Override
public void setFrom(Address from) throws MessagingException {
this.mFrom = new Address[] { from };
mMessageDirty = true;
}
@ -219,7 +187,6 @@ public class LocalMessage extends MimeMessage {
} else {
mReplyTo = replyTo;
}
mMessageDirty = true;
}
@ -250,7 +217,6 @@ public class LocalMessage extends MimeMessage {
} else {
throw new MessagingException("Unrecognized recipient type.");
}
mMessageDirty = true;
}
public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
@ -301,23 +267,14 @@ public class LocalMessage extends MimeMessage {
}
/*
* If a message is being marked as deleted we want to clear out it's content
* and attachments as well. Delete will not actually remove the row since we need
* to retain the uid for synchronization purposes.
* If a message is being marked as deleted we want to clear out its content. Delete will not actually remove the
* row since we need to retain the UID for synchronization purposes.
*/
private void delete() throws MessagingException
{
/*
* Delete all of the message's content to save space.
*/
private void delete() throws MessagingException {
try {
this.localStore.database.execute(true, new DbCallback<Void>() {
localStore.database.execute(true, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
UnavailableStorageException {
String[] idArg = new String[] { Long.toString(mId) };
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
ContentValues cv = new ContentValues();
cv.put("deleted", 1);
cv.put("empty", 1);
@ -328,33 +285,25 @@ public class LocalMessage extends MimeMessage {
cv.putNull("cc_list");
cv.putNull("bcc_list");
cv.putNull("preview");
cv.putNull("html_content");
cv.putNull("text_content");
cv.putNull("reply_to_list");
cv.putNull("message_part_id");
db.update("messages", cv, "id = ?", idArg);
db.update("messages", cv, "id = ?", new String[] { Long.toString(mId) });
/*
* Delete all of the message's attachments to save space.
* We do this explicit deletion here because we're not deleting the record
* in messages, which means our ON DELETE trigger for messages won't cascade
*/
try {
((LocalFolder) mFolder).deleteAttachments(mId);
((LocalFolder) mFolder).deleteMessagePartsAndDataFromDisk(messagePartId);
} catch (MessagingException e) {
throw new WrappedException(e);
}
db.delete("attachments", "message_id = ?", idArg);
return null;
}
});
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
throw (MessagingException) e.getCause();
}
((LocalFolder)mFolder).deleteHeaders(mId);
this.localStore.notifyChange();
localStore.notifyChange();
}
/*
@ -372,7 +321,7 @@ public class LocalMessage extends MimeMessage {
try {
LocalFolder localFolder = (LocalFolder) mFolder;
localFolder.deleteAttachments(mId);
localFolder.deleteMessagePartsAndDataFromDisk(messagePartId);
if (hasThreadChildren(db, mId)) {
// This message has children in the thread structure so we need to
@ -500,23 +449,8 @@ public class LocalMessage extends MimeMessage {
}
private void loadHeaders() throws MessagingException {
List<LocalMessage> messages = new ArrayList<LocalMessage>();
messages.add(this);
mHeadersLoaded = true; // set true before calling populate headers to stop recursion
getFolder().populateHeaders(messages);
}
@Override
public void addHeader(String name, String value) throws MessagingException {
if (!mHeadersLoaded)
loadHeaders();
super.addHeader(name, value);
}
@Override
public void addRawHeader(String name, String raw) {
throw new RuntimeException("Not supported");
mHeadersLoaded = true;
getFolder().populateHeaders(this);
}
@Override
@ -557,7 +491,6 @@ public class LocalMessage extends MimeMessage {
message.mSubject = mSubject;
message.mPreview = mPreview;
message.mHeadersLoaded = mHeadersLoaded;
message.mMessageDirty = mMessageDirty;
return message;
}
@ -621,4 +554,8 @@ public class LocalMessage extends MimeMessage {
private String getAccountUuid() {
return getAccount().getUuid();
}
public boolean isBodyMissing() {
return getBody() == null;
}
}

View File

@ -1,19 +1,28 @@
package com.fsck.k9.mailstore;
import android.content.Context;
import android.net.Uri;
import com.fsck.k9.R;
import com.fsck.k9.crypto.DecryptedTempFileBody;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.Viewable;
import com.fsck.k9.mailstore.MessageViewInfo.MessageViewContainer;
import com.fsck.k9.provider.AttachmentProvider;
import com.fsck.k9.provider.K9FileProvider;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -25,7 +34,7 @@ import static com.fsck.k9.mail.internet.Viewable.MessageHeader;
import static com.fsck.k9.mail.internet.Viewable.Text;
import static com.fsck.k9.mail.internet.Viewable.Textual;
class LocalMessageExtractor {
public class LocalMessageExtractor {
private static final String TEXT_DIVIDER =
"------------------------------------------------------------------------";
private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length();
@ -33,24 +42,26 @@ class LocalMessageExtractor {
private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length();
private static final String FILENAME_SUFFIX = " ";
private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length();
private static final OpenPgpResultAnnotation NO_ANNOTATIONS = null;
private LocalMessageExtractor() {}
/**
* Extract the viewable textual parts of a message and return the rest as attachments.
*
* @param context A {@link android.content.Context} instance that will be used to get localized strings.
* @param viewables
* @param attachments
* @return A {@link ViewableContainer} instance containing the textual parts of the message as
* plain text and HTML, and a list of message parts considered attachments.
*
* @throws com.fsck.k9.mail.MessagingException
* In case of an error.
*/
public static ViewableContainer extractTextAndAttachments(Context context, Message message) throws MessagingException {
public static ViewableContainer extractTextAndAttachments(Context context, List<Viewable> viewables,
List<Part> attachments) throws MessagingException {
try {
List<Part> attachments = new ArrayList<Part>();
// Collect all viewable parts
List<Viewable> viewables = MessageExtractor.getViewables(message, attachments);
/*
* Convert the tree of viewable parts into text and HTML
@ -118,35 +129,6 @@ class LocalMessageExtractor {
}
}
public static ViewableContainer extractPartsFromDraft(Message message)
throws MessagingException {
Body body = message.getBody();
if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) {
MimeMultipart multipart = (MimeMultipart) body;
ViewableContainer container;
int count = multipart.getCount();
if (count >= 1) {
// The first part is either a text/plain or a multipart/alternative
BodyPart firstPart = multipart.getBodyPart(0);
container = extractTextual(firstPart);
// The rest should be attachments
for (int i = 1; i < count; i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
container.attachments.add(bodyPart);
}
} else {
container = new ViewableContainer("", "", new ArrayList<Part>());
}
return container;
}
return extractTextual(message);
}
/**
* Use the contents of a {@link com.fsck.k9.mail.internet.Viewable} to create the HTML to be displayed.
*
@ -439,32 +421,167 @@ class LocalMessageExtractor {
html.append("</td></tr>");
}
private static ViewableContainer extractTextual(Part part) throws MessagingException {
String text = "";
String html = "";
List<Part> attachments = new ArrayList<Part>();
public static MessageViewInfo decodeMessageForView(Context context,
Message message, MessageCryptoAnnotations annotations) throws MessagingException {
Body firstBody = part.getBody();
if (part.isMimeType("text/plain")) {
String bodyText = MessageExtractor.getTextFromPart(part);
if (bodyText != null) {
text = bodyText;
html = HtmlConverter.textToHtml(text);
// 1. break mime structure on encryption/signature boundaries
List<Part> parts = getCryptPieces(message, annotations);
// 2. extract viewables/attachments of parts
ArrayList<MessageViewContainer> containers = new ArrayList<MessageViewContainer>();
for (Part part : parts) {
OpenPgpResultAnnotation pgpAnnotation = annotations.get(part);
// TODO properly handle decrypted data part - this just replaces the part
if (pgpAnnotation != NO_ANNOTATIONS && pgpAnnotation.hasOutputData()) {
part = pgpAnnotation.getOutputData();
}
} else if (part.isMimeType("multipart/alternative") &&
firstBody instanceof MimeMultipart) {
MimeMultipart multipart = (MimeMultipart) firstBody;
for (BodyPart bodyPart : multipart.getBodyParts()) {
String bodyText = MessageExtractor.getTextFromPart(bodyPart);
if (bodyText != null) {
if (text.isEmpty() && bodyPart.isMimeType("text/plain")) {
text = bodyText;
} else if (html.isEmpty() && bodyPart.isMimeType("text/html")) {
html = bodyText;
}
ArrayList<Part> attachments = new ArrayList<Part>();
List<Viewable> viewables = MessageExtractor.getViewables(part, attachments);
// 3. parse viewables into html string
ViewableContainer viewable = LocalMessageExtractor.extractTextAndAttachments(context, viewables,
attachments);
List<AttachmentViewInfo> attachmentInfos = extractAttachmentInfos(context, attachments);
MessageViewContainer messageViewContainer =
new MessageViewContainer(viewable.html, part, attachmentInfos, pgpAnnotation);
containers.add(messageViewContainer);
}
return new MessageViewInfo(containers, message);
}
public static List<Part> getCryptPieces(Message message, MessageCryptoAnnotations annotations) throws MessagingException {
// TODO make sure this method does what it is supposed to
/* This method returns a list of mime parts which are to be parsed into
* individual MessageViewContainers for display, which each have their
* own crypto header. This means parts should be individual for each
* multipart/encrypted, multipart/signed, or a multipart/* which does
* not contain children of the former types.
*/
ArrayList<Part> parts = new ArrayList<Part>();
if (!getCryptSubPieces(message, parts, annotations)) {
parts.add(message);
}
return parts;
}
public static boolean getCryptSubPieces(Part part, ArrayList<Part> parts,
MessageCryptoAnnotations annotations) throws MessagingException {
Body body = part.getBody();
if (body instanceof Multipart) {
Multipart multi = (Multipart) body;
if ("multipart/mixed".equals(part.getMimeType())) {
boolean foundSome = false;
for (BodyPart sub : multi.getBodyParts()) {
foundSome |= getCryptSubPieces(sub, parts, annotations);
}
if (!foundSome) {
parts.add(part);
return true;
}
} else if (annotations.has(part)) {
parts.add(part);
return true;
}
}
return new ViewableContainer(text, html, attachments);
return false;
}
private static List<AttachmentViewInfo> extractAttachmentInfos(Context context, List<Part> attachmentParts)
throws MessagingException {
List<AttachmentViewInfo> attachments = new ArrayList<AttachmentViewInfo>();
for (Part part : attachmentParts) {
attachments.add(extractAttachmentInfo(context, part));
}
return attachments;
}
public static AttachmentViewInfo extractAttachmentInfo(Context context, Part part) throws MessagingException {
if (part instanceof LocalPart) {
LocalPart localPart = (LocalPart) part;
String accountUuid = localPart.getAccountUuid();
long messagePartId = localPart.getId();
String mimeType = part.getMimeType();
String displayName = localPart.getDisplayName();
long size = localPart.getSize();
boolean firstClassAttachment = localPart.isFirstClassAttachment();
Uri uri = AttachmentProvider.getAttachmentUri(accountUuid, messagePartId);
return new AttachmentViewInfo(mimeType, displayName, size, uri, firstClassAttachment, part);
} else {
Body body = part.getBody();
if (body instanceof DecryptedTempFileBody) {
DecryptedTempFileBody decryptedTempFileBody = (DecryptedTempFileBody) body;
File file = decryptedTempFileBody.getFile();
Uri uri = K9FileProvider.getUriForFile(context, file, part.getMimeType());
long size = file.length();
return extractAttachmentInfo(part, uri, size);
} else {
throw new RuntimeException("Not supported");
}
}
}
public static AttachmentViewInfo extractAttachmentInfo(Part part) throws MessagingException {
return extractAttachmentInfo(part, Uri.EMPTY, AttachmentViewInfo.UNKNOWN_SIZE);
}
private static AttachmentViewInfo extractAttachmentInfo(Part part, Uri uri, long size) throws MessagingException {
boolean firstClassAttachment = true;
String mimeType = part.getMimeType();
String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType());
String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
String name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
if (name == null) {
name = MimeUtility.getHeaderParameter(contentTypeHeader, "name");
}
if (name == null) {
firstClassAttachment = false;
String extension = MimeUtility.getExtensionByMimeType(mimeType);
name = "noname" + ((extension != null) ? "." + extension : "");
}
// Inline parts with a content-id are almost certainly components of an HTML message
// not attachments. Only show them if the user pressed the button to show more
// attachments.
if (contentDisposition != null &&
MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)") &&
part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) {
firstClassAttachment = false;
}
long attachmentSize = extractAttachmentSize(contentDisposition, size);
return new AttachmentViewInfo(mimeType, name, attachmentSize, uri, firstClassAttachment, part);
}
private static long extractAttachmentSize(String contentDisposition, long size) {
if (size != AttachmentViewInfo.UNKNOWN_SIZE) {
return size;
}
long result = AttachmentViewInfo.UNKNOWN_SIZE;
String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size");
if (sizeParam != null) {
try {
result = Integer.parseInt(sizeParam);
} catch (NumberFormatException e) { /* ignore */ }
}
return result;
}
}

View File

@ -0,0 +1,11 @@
package com.fsck.k9.mailstore;
public interface LocalPart {
String getAccountUuid();
long getId();
String getDisplayName();
long getSize();
boolean isFirstClassAttachment();
LocalMessage getMessage();
}

View File

@ -21,6 +21,7 @@ import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mailstore.LocalFolder.DataLocation;
import com.fsck.k9.mailstore.StorageManager.StorageProvider;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
@ -30,8 +31,16 @@ import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.search.SqlQueryBuilder;
import org.apache.james.mime4j.codec.Base64InputStream;
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
import org.apache.james.mime4j.util.MimeUtil;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
@ -75,7 +84,7 @@ public class LocalStore extends Store implements Serializable {
"subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " +
"bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " +
"folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " +
"forwarded ";
"forwarded, message_part_id, mime_type ";
static final String GET_FOLDER_COLS =
"folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " +
@ -118,7 +127,7 @@ public class LocalStore extends Store implements Serializable {
*/
private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500;
public static final int DB_VERSION = 50;
public static final int DB_VERSION = 51;
public static String getColumnNameForFlag(Flag flag) {
@ -275,7 +284,7 @@ public class LocalStore extends Store implements Serializable {
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Before prune size = " + getSize());
pruneCachedAttachments(true);
deleteAllMessageDataFromDisk();
if (K9.DEBUG) {
Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize());
@ -284,24 +293,16 @@ public class LocalStore extends Store implements Serializable {
Log.i(K9.LOG_TAG, "After prune / before clear size = " + getSize());
}
// don't delete messages that are Local, since there is no copy on the server.
// Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have
// been deleted locally. They take up insignificant space
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) {
// Delete entries from 'threads' table
db.execSQL("DELETE FROM threads WHERE message_id IN " +
"(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')");
// We don't care about threads of deleted messages, so delete the whole table.
db.delete("threads", null, null);
// Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make
// sure the thread structure is in a valid state (this may destroy existing valid
// thread trees, but is much faster than adjusting the tree by removing messages
// one by one).
db.execSQL("UPDATE threads SET root=id, parent=NULL");
// Delete entries from 'messages' table
db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'");
// Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have
// been deleted locally.
db.delete("messages", "deleted = 0", null);
return null;
}
});
@ -405,73 +406,39 @@ public class LocalStore extends Store implements Serializable {
database.recreate();
}
/**
* Deletes all cached attachments for the entire store.
* @param force
* @throws com.fsck.k9.mail.MessagingException
*/
//TODO this method seems to be only called with force=true, simplify accordingly
private void pruneCachedAttachments(final boolean force) throws MessagingException {
private void deleteAllMessageDataFromDisk() throws MessagingException {
markAllMessagePartsDataAsMissing();
deleteAllMessagePartsDataFromDisk();
}
private void markAllMessagePartsDataAsMissing() throws MessagingException {
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
if (force) {
ContentValues cv = new ContentValues();
cv.putNull("content_uri");
db.update("attachments", cv, null, null);
}
final StorageManager storageManager = StorageManager.getInstance(context);
File[] files = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()).listFiles();
for (File file : files) {
if (file.exists()) {
if (!force) {
Cursor cursor = null;
try {
cursor = db.query(
"attachments",
new String[] { "store_data" },
"id = ?",
new String[] { file.getName() },
null,
null,
null);
if (cursor.moveToNext()) {
if (cursor.getString(0) == null) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Attachment " + file.getAbsolutePath() + " has no store data, not deleting");
/*
* If the attachment has no store data it is not recoverable, so
* we won't delete it.
*/
continue;
}
}
} finally {
Utility.closeQuietly(cursor);
}
}
if (!force) {
try {
ContentValues cv = new ContentValues();
cv.putNull("content_uri");
db.update("attachments", cv, "id = ?", new String[] { file.getName() });
} catch (Exception e) {
/*
* If the row has gone away before we got to mark it not-downloaded that's
* okay.
*/
}
}
if (!file.delete()) {
file.deleteOnExit();
}
}
}
ContentValues cv = new ContentValues();
cv.put("data_location", DataLocation.MISSING);
db.update("message_parts", cv, null, null);
return null;
}
});
}
private void deleteAllMessagePartsDataFromDisk() {
final StorageManager storageManager = StorageManager.getInstance(context);
File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId());
File[] files = attachmentDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.exists() && !file.delete()) {
file.deleteOnExit();
}
}
}
public void resetVisibleLimits(int visibleLimit) throws MessagingException {
final ContentValues cv = new ContentValues();
cv.put("visible_limit", Integer.toString(visibleLimit));
@ -679,40 +646,110 @@ public class LocalStore extends Store implements Serializable {
return database.execute(false, new DbCallback<AttachmentInfo>() {
@Override
public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException {
String name;
String type;
int size;
Cursor cursor = null;
Cursor cursor = db.query("message_parts",
new String[] { "display_name", "decoded_body_size", "mime_type" },
"id = ?",
new String[] { attachmentId },
null, null, null);
try {
cursor = db.query(
"attachments",
new String[] { "name", "size", "mime_type" },
"id = ?",
new String[] { attachmentId },
null,
null,
null);
if (!cursor.moveToFirst()) {
return null;
}
name = cursor.getString(0);
size = cursor.getInt(1);
type = cursor.getString(2);
String name = cursor.getString(0);
long size = cursor.getLong(1);
String mimeType = cursor.getString(2);
final AttachmentInfo attachmentInfo = new AttachmentInfo();
attachmentInfo.name = name;
attachmentInfo.size = size;
attachmentInfo.type = type;
attachmentInfo.type = mimeType;
return attachmentInfo;
} finally {
Utility.closeQuietly(cursor);
cursor.close();
}
}
});
}
public InputStream getAttachmentInputStream(final String attachmentId) throws MessagingException {
return database.execute(false, new DbCallback<InputStream>() {
@Override
public InputStream doDbWork(final SQLiteDatabase db) throws WrappedException {
Cursor cursor = db.query("message_parts",
new String[] { "data_location", "data", "encoding" },
"id = ?",
new String[] { attachmentId },
null, null, null);
try {
if (!cursor.moveToFirst()) {
return null;
}
int location = cursor.getInt(0);
String encoding = cursor.getString(2);
InputStream rawInputStream = getRawAttachmentInputStream(cursor, location, attachmentId);
return getDecodingInputStream(rawInputStream, encoding);
} finally {
cursor.close();
}
}
});
}
private InputStream getRawAttachmentInputStream(Cursor cursor, int location, String attachmentId) {
switch (location) {
case DataLocation.IN_DATABASE: {
byte[] data = cursor.getBlob(1);
return new ByteArrayInputStream(data);
}
case DataLocation.ON_DISK: {
File file = getAttachmentFile(attachmentId);
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new WrappedException(e);
}
}
default: {
throw new IllegalStateException("No attachment data available");
}
}
}
InputStream getDecodingInputStream(final InputStream rawInputStream, String encoding) {
if (MimeUtil.ENC_BASE64.equals(encoding)) {
return new Base64InputStream(rawInputStream) {
@Override
public void close() throws IOException {
super.close();
rawInputStream.close();
}
};
}
if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(encoding)) {
return new QuotedPrintableInputStream(rawInputStream) {
@Override
public void close() throws IOException {
super.close();
rawInputStream.close();
}
};
}
return rawInputStream;
}
File getAttachmentFile(String attachmentId) {
final StorageManager storageManager = StorageManager.getInstance(context);
final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId());
return new File(attachmentDirectory, attachmentId);
}
public static class AttachmentInfo {
public String name;
public int size;
public long size;
public String type;
}

View File

@ -1,20 +0,0 @@
package com.fsck.k9.mailstore;
import com.fsck.k9.mail.internet.TextBody;
class LocalTextBody extends TextBody {
/**
* This is an HTML-ified version of the message for display purposes.
*/
private final String mBodyForDisplay;
public LocalTextBody(String body, String bodyForDisplay) {
super(body);
this.mBodyForDisplay = bodyForDisplay;
}
public String getBodyForDisplay() {
return mBodyForDisplay;
}
}//LocalTextBody

View File

@ -0,0 +1,47 @@
package com.fsck.k9.mailstore;
import java.util.Stack;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeBodyPart;
public class MessageHelper {
public static boolean isCompletePartAvailable(Part part) {
Stack<Part> partsToCheck = new Stack<Part>();
partsToCheck.push(part);
while (!partsToCheck.isEmpty()) {
Part currentPart = partsToCheck.pop();
Body body = currentPart.getBody();
boolean isBodyMissing = body == null;
if (isBodyMissing) {
return false;
}
if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
for (BodyPart bodyPart : multipart.getBodyParts()) {
partsToCheck.push(bodyPart);
}
}
}
return true;
}
public static MimeBodyPart createEmptyPart() {
try {
return new MimeBodyPart(null);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fsck.k9.mailstore;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.Viewable;
class MessageInfoExtractor {
private final Context context;
private final Message message;
private List<Viewable> viewables;
private List<Part> attachments;
public MessageInfoExtractor(Context context, Message message) {
this.context = context;
this.message = message;
}
public String getMessageTextPreview() throws MessagingException {
getViewablesIfNecessary();
return MessagePreviewExtractor.extractPreview(context, viewables);
}
public int getAttachmentCount() throws MessagingException {
getViewablesIfNecessary();
return attachments.size();
}
private void getViewablesIfNecessary() throws MessagingException {
if (viewables == null) {
attachments = new ArrayList<Part>();
viewables = MessageExtractor.getViewables(message, attachments);
}
}
}

View File

@ -0,0 +1,154 @@
package com.fsck.k9.mailstore;
import java.util.List;
import android.content.Context;
import android.text.TextUtils;
import com.fsck.k9.R;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.Viewable;
import com.fsck.k9.mail.internet.Viewable.Alternative;
import com.fsck.k9.mail.internet.Viewable.Html;
import com.fsck.k9.mail.internet.Viewable.MessageHeader;
import com.fsck.k9.mail.internet.Viewable.Textual;
class MessagePreviewExtractor {
private static final int MAX_PREVIEW_LENGTH = 512;
private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192;
public static String extractPreview(Context context, List<Viewable> viewables) {
StringBuilder text = new StringBuilder();
boolean divider = false;
for (Viewable viewable : viewables) {
if (viewable instanceof Textual) {
appendText(text, viewable, divider);
divider = true;
} else if (viewable instanceof MessageHeader) {
appendMessagePreview(context, text, (MessageHeader) viewable, divider);
divider = false;
} else if (viewable instanceof Alternative) {
appendAlternative(text, (Alternative) viewable, divider);
divider = true;
}
if (hasMaxPreviewLengthBeenReached(text)) {
break;
}
}
if (hasMaxPreviewLengthBeenReached(text)) {
text.setLength(MAX_PREVIEW_LENGTH - 1);
text.append('…');
}
return text.toString();
}
private static void appendText(StringBuilder text, Viewable viewable, boolean prependDivider) {
if (viewable instanceof Textual) {
appendTextual(text, (Textual) viewable, prependDivider);
} else if (viewable instanceof Alternative) {
appendAlternative(text, (Alternative) viewable, prependDivider);
} else {
throw new IllegalArgumentException("Unknown Viewable");
}
}
private static void appendTextual(StringBuilder text, Textual textual, boolean prependDivider) {
Part part = textual.getPart();
if (prependDivider) {
appendDivider(text);
}
String textFromPart = MessageExtractor.getTextFromPart(part);
if (textFromPart == null) {
textFromPart = "";
} else if (textual instanceof Html) {
textFromPart = HtmlConverter.htmlToText(textFromPart);
}
text.append(stripTextForPreview(textFromPart));
}
private static void appendAlternative(StringBuilder text, Alternative alternative, boolean prependDivider) {
List<Viewable> textAlternative = alternative.getText().isEmpty() ?
alternative.getHtml() : alternative.getText();
boolean divider = prependDivider;
for (Viewable textViewable : textAlternative) {
appendText(text, textViewable, divider);
divider = true;
if (hasMaxPreviewLengthBeenReached(text)) {
break;
}
}
}
private static void appendMessagePreview(Context context, StringBuilder text, MessageHeader messageHeader,
boolean divider) {
if (divider) {
appendDivider(text);
}
String subject = messageHeader.getMessage().getSubject();
if (TextUtils.isEmpty(subject)) {
text.append(context.getString(R.string.preview_untitled_inner_message));
} else {
text.append(context.getString(R.string.preview_inner_message, subject));
}
}
private static void appendDivider(StringBuilder text) {
text.append(" / ");
}
private static String stripTextForPreview(String text) {
if (text == null) {
return "";
}
// Only look at the first 8k of a message when calculating
// the preview. This should avoid unnecessary
// memory usage on large messages
if (text.length() > MAX_CHARACTERS_CHECKED_FOR_PREVIEW) {
text = text.substring(0, MAX_CHARACTERS_CHECKED_FOR_PREVIEW);
}
// Remove (correctly delimited by '-- \n') signatures
text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", "");
// try to remove lines of dashes in the preview
text = text.replaceAll("(?m)^----.*?$", "");
// remove quoted text from the preview
text = text.replaceAll("(?m)^[#>].*$", "");
// Remove a common quote header from the preview
text = text.replaceAll("(?m)^On .*wrote.?$", "");
// Remove a more generic quote header from the preview
text = text.replaceAll("(?m)^.*\\w+:$", "");
// Remove horizontal rules.
text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " ");
// URLs in the preview should just be shown as "..." - They're not
// clickable and they usually overwhelm the preview
text = text.replaceAll("https?://\\S+", "...");
// Don't show newlines in the preview
text = text.replaceAll("(\\r|\\n)+", " ");
// Collapse whitespace in the preview
text = text.replaceAll("\\s+", " ");
// Remove any whitespace at the beginning and end of the string.
text = text.trim();
return (text.length() <= MAX_PREVIEW_LENGTH) ? text : text.substring(0, MAX_PREVIEW_LENGTH);
}
private static boolean hasMaxPreviewLengthBeenReached(StringBuilder text) {
return text.length() >= MAX_PREVIEW_LENGTH;
}
}

View File

@ -0,0 +1,36 @@
package com.fsck.k9.mailstore;
import java.util.List;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
public class MessageViewInfo {
public final Message message;
public final List<MessageViewContainer> containers;
public MessageViewInfo(List<MessageViewContainer> containers, Message message) {
this.containers = containers;
this.message = message;
}
public static class MessageViewContainer {
public final String text;
public final Part rootPart;
public final List<AttachmentViewInfo> attachments;
public final OpenPgpResultAnnotation cryptoAnnotation;
MessageViewContainer(String text, Part rootPart, List<AttachmentViewInfo> attachments,
OpenPgpResultAnnotation cryptoAnnotation) {
this.text = text;
this.rootPart = rootPart;
this.attachments = attachments;
this.cryptoAnnotation = cryptoAnnotation;
}
}
}

View File

@ -0,0 +1,79 @@
package com.fsck.k9.mailstore;
import android.app.PendingIntent;
import com.fsck.k9.mail.internet.MimeBodyPart;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
public final class OpenPgpResultAnnotation {
private boolean wasEncrypted;
private OpenPgpSignatureResult signatureResult;
private OpenPgpError error;
private CryptoError errorType = CryptoError.NONE;
private PendingIntent pendingIntent;
private MimeBodyPart outputData;
public OpenPgpSignatureResult getSignatureResult() {
return signatureResult;
}
public PendingIntent getPendingIntent() {
return pendingIntent;
}
public void setSignatureResult(OpenPgpSignatureResult signatureResult) {
this.signatureResult = signatureResult;
}
public void setPendingIntent(PendingIntent pendingIntent) {
this.pendingIntent = pendingIntent;
}
public OpenPgpError getError() {
return error;
}
public void setError(OpenPgpError error) {
this.error = error;
setErrorType(CryptoError.CRYPTO_API_RETURNED_ERROR);
}
public CryptoError getErrorType() {
return errorType;
}
public void setErrorType(CryptoError errorType) {
this.errorType = errorType;
}
public boolean hasOutputData() {
return outputData != null;
}
public void setOutputData(MimeBodyPart outputData) {
this.outputData = outputData;
}
public MimeBodyPart getOutputData() {
return outputData;
}
public boolean wasEncrypted() {
return wasEncrypted;
}
public void setWasEncrypted(boolean wasEncrypted) {
this.wasEncrypted = wasEncrypted;
}
public static enum CryptoError {
NONE,
CRYPTO_API_RETURNED_ERROR,
SIGNED_BUT_INCOMPLETE,
ENCRYPTED_BUT_INCOMPLETE
}
}

View File

@ -53,8 +53,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
Log.i(K9.LOG_TAG, String.format(Locale.US, "Upgrading database from version %d to version %d",
db.getVersion(), LocalStore.DB_VERSION));
AttachmentProvider.clear(this.localStore.context);
db.beginTransaction();
try {
// schema version 29 was when we moved to incremental updates
@ -83,8 +81,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
"cc_list TEXT, " +
"bcc_list TEXT, " +
"reply_to_list TEXT, " +
"html_content TEXT, " +
"text_content TEXT, " +
"attachment_count INTEGER, " +
"internal_date INTEGER, " +
"message_id TEXT, " +
@ -95,12 +91,36 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
"read INTEGER default 0, " +
"flagged INTEGER default 0, " +
"answered INTEGER default 0, " +
"forwarded INTEGER default 0" +
"forwarded INTEGER default 0, " +
"message_part_id INTEGER" +
")");
db.execSQL("DROP TABLE IF EXISTS headers");
db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)");
db.execSQL("CREATE INDEX IF NOT EXISTS header_folder ON headers (message_id)");
db.execSQL("CREATE TABLE message_parts (" +
"id INTEGER PRIMARY KEY, " +
"type INTEGER NOT NULL, " +
"root INTEGER, " +
"parent INTEGER NOT NULL, " +
"seq INTEGER NOT NULL, " +
"mime_type TEXT, " +
"decoded_body_size INTEGER, " +
"display_name TEXT, " +
"header TEXT, " +
"encoding TEXT, " +
"charset TEXT, " +
"data_location INTEGER NOT NULL, " +
"data BLOB, " +
"preamble TEXT, " +
"epilogue TEXT, " +
"boundary TEXT, " +
"content_id TEXT, " +
"server_extra TEXT" +
")");
db.execSQL("CREATE TRIGGER set_message_part_root " +
"AFTER INSERT ON message_parts " +
"BEGIN " +
"UPDATE message_parts SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " +
"END");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)");
db.execSQL("DROP INDEX IF EXISTS msg_folder_id");
@ -145,11 +165,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
"UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " +
"END");
db.execSQL("DROP TABLE IF EXISTS attachments");
db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
+ "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
+ "mime_type TEXT, content_id TEXT, content_disposition TEXT)");
db.execSQL("DROP TABLE IF EXISTS pending_commands");
db.execSQL("CREATE TABLE pending_commands " +
"(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)");
@ -158,8 +173,11 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
db.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;");
db.execSQL("DROP TRIGGER IF EXISTS delete_message");
db.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; "
+ "DELETE FROM headers where old.id = message_id; END;");
db.execSQL("CREATE TRIGGER delete_message " +
"BEFORE DELETE ON messages " +
"BEGIN " +
"DELETE FROM message_parts WHERE root = OLD.message_part_id;" +
"END");
} else {
// in the case that we're starting out at 29 or newer, run all the needed updates
@ -541,6 +559,9 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
db.update("folders", cv, "name = ?",
new String[] { this.localStore.getAccount().getInboxFolderName() });
}
if (db.getVersion() < 51) {
throw new IllegalStateException("Database upgrade not supported yet!");
}
}
db.setVersion(LocalStore.DB_VERSION);

View File

@ -7,11 +7,13 @@ import java.io.FileNotFoundException;
import java.io.InputStream;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.SizeAware;
/**
* An attachment whose contents are contained in a file.
*/
public class TempFileBody extends BinaryAttachmentBody {
public class TempFileBody extends BinaryAttachmentBody implements SizeAware {
private final File mFile;
public TempFileBody(String filename) {
@ -26,4 +28,9 @@ public class TempFileBody extends BinaryAttachmentBody {
return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY);
}
}
@Override
public long getSize() {
return mFile.length();
}
}

View File

@ -9,8 +9,7 @@ import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException;
/**
* An attachment containing a body of type message/rfc822
* whose contents are contained in a file.
* An attachment containing a body of type message/rfc822 whose contents are contained in a file.
*/
public class TempFileMessageBody extends TempFileBody implements CompositeBody {
@ -25,7 +24,12 @@ public class TempFileMessageBody extends TempFileBody implements CompositeBody {
@Override
public void setUsing7bitTransport() throws MessagingException {
// see LocalAttachmentMessageBody.setUsing7bitTransport()
/*
* There's nothing to recurse into here, so there's nothing to do.
* The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once
* writeTo() is called, the file with the rfc822 body will be opened
* for reading and will then be recursed.
*/
}
@Override

View File

@ -10,7 +10,7 @@ import java.util.List;
*
* @see LocalMessageExtractor#extractTextAndAttachments(android.content.Context, com.fsck.k9.mail.Message)
*/
class ViewableContainer {
public class ViewableContainer {
/**
* The viewable text of the message in plain text.
*/

View File

@ -0,0 +1,49 @@
package com.fsck.k9.message;
// FYI, there's nothing in the code that requires these variables to one letter. They're one
// letter simply to save space. This name sucks. It's too similar to Account.Identity.
public enum IdentityField {
LENGTH("l"),
OFFSET("o"),
FOOTER_OFFSET("fo"),
PLAIN_LENGTH("pl"),
PLAIN_OFFSET("po"),
MESSAGE_FORMAT("f"),
MESSAGE_READ_RECEIPT("r"),
SIGNATURE("s"),
NAME("n"),
EMAIL("e"),
// TODO - store a reference to the message being replied so we can mark it at the time of send.
ORIGINAL_MESSAGE("m"),
CURSOR_POSITION("p"), // Where in the message your cursor was when you saved.
QUOTED_TEXT_MODE("q"),
QUOTE_STYLE("qs");
private final String value;
IdentityField(String value) {
this.value = value;
}
public String value() {
return value;
}
/**
* Get the list of IdentityFields that should be integer values.
*
* <p>
* These values are sanity checked for integer-ness during decoding.
* </p>
*
* @return The list of integer {@link IdentityField}s.
*/
public static IdentityField[] getIntegerFields() {
return new IdentityField[] { LENGTH, OFFSET, FOOTER_OFFSET, PLAIN_LENGTH, PLAIN_OFFSET };
}
// Version identifier for "new style" identity. ! is an impossible value in base64 encoding, so we
// use that to determine which version we're in.
static final String IDENTITY_VERSION_1 = "!";
}

View File

@ -0,0 +1,181 @@
package com.fsck.k9.message;
import android.net.Uri;
import android.net.Uri.Builder;
import android.util.Log;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.mail.internet.TextBody;
public class IdentityHeaderBuilder {
private InsertableHtmlContent quotedHtmlContent;
private QuoteStyle quoteStyle;
private SimpleMessageFormat messageFormat;
private Identity identity;
private boolean signatureChanged;
private String signature;
private boolean identityChanged;
private QuotedTextMode quotedTextMode;
private MessageReference messageReference;
private TextBody body;
private TextBody bodyPlain;
private int cursorPosition;
private Builder uri;
/**
* Build the identity header string. This string contains metadata about a draft message to be
* used upon loading a draft for composition. This should be generated at the time of saving a
* draft.<br>
* <br>
* This is a URL-encoded key/value pair string. The list of possible values are in {@link IdentityField}.
*
* @return Identity string.
*/
public String build() {
//FIXME: check arguments
uri = new Uri.Builder();
if (body.getComposedMessageLength() != null && body.getComposedMessageOffset() != null) {
// See if the message body length is already in the TextBody.
appendValue(IdentityField.LENGTH, body.getComposedMessageLength());
appendValue(IdentityField.OFFSET, body.getComposedMessageOffset());
} else {
// If not, calculate it now.
appendValue(IdentityField.LENGTH, body.getText().length());
appendValue(IdentityField.OFFSET, 0);
}
if (quotedHtmlContent != null) {
appendValue(IdentityField.FOOTER_OFFSET, quotedHtmlContent.getFooterInsertionPoint());
}
if (bodyPlain != null) {
Integer composedMessageLength = bodyPlain.getComposedMessageLength();
Integer composedMessageOffset = bodyPlain.getComposedMessageOffset();
if (composedMessageLength != null && composedMessageOffset != null) {
// See if the message body length is already in the TextBody.
appendValue(IdentityField.PLAIN_LENGTH, composedMessageLength);
appendValue(IdentityField.PLAIN_OFFSET, composedMessageOffset);
} else {
// If not, calculate it now.
appendValue(IdentityField.PLAIN_LENGTH, body.getText().length());
appendValue(IdentityField.PLAIN_OFFSET, 0);
}
}
// Save the quote style (useful for forwards).
appendValue(IdentityField.QUOTE_STYLE, quoteStyle);
// Save the message format for this offset.
appendValue(IdentityField.MESSAGE_FORMAT, messageFormat);
// If we're not using the standard identity of signature, append it on to the identity blob.
if (identity.getSignatureUse() && signatureChanged) {
appendValue(IdentityField.SIGNATURE, signature);
}
if (identityChanged) {
appendValue(IdentityField.NAME, identity.getName());
appendValue(IdentityField.EMAIL, identity.getEmail());
}
if (messageReference != null) {
appendValue(IdentityField.ORIGINAL_MESSAGE, messageReference.toIdentityString());
}
appendValue(IdentityField.CURSOR_POSITION, cursorPosition);
appendValue(IdentityField.QUOTED_TEXT_MODE, quotedTextMode);
String k9identity = IdentityField.IDENTITY_VERSION_1 + uri.build().getEncodedQuery();
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Generated identity: " + k9identity);
}
return k9identity;
}
private void appendValue(IdentityField field, int value) {
appendValue(field, Integer.toString(value));
}
private void appendValue(IdentityField field, Integer value) {
appendValue(field, value.toString());
}
private void appendValue(IdentityField field, Enum<?> value) {
appendValue(field, value.name());
}
private void appendValue(IdentityField field, String value) {
uri.appendQueryParameter(field.value(), value);
}
public IdentityHeaderBuilder setQuotedHtmlContent(InsertableHtmlContent quotedHtmlContent) {
this.quotedHtmlContent = quotedHtmlContent;
return this;
}
public IdentityHeaderBuilder setQuoteStyle(QuoteStyle quoteStyle) {
this.quoteStyle = quoteStyle;
return this;
}
public IdentityHeaderBuilder setQuoteTextMode(QuotedTextMode quotedTextMode) {
this.quotedTextMode = quotedTextMode;
return this;
}
public IdentityHeaderBuilder setMessageFormat(SimpleMessageFormat messageFormat) {
this.messageFormat = messageFormat;
return this;
}
public IdentityHeaderBuilder setIdentity(Identity identity) {
this.identity = identity;
return this;
}
public IdentityHeaderBuilder setIdentityChanged(boolean identityChanged) {
this.identityChanged = identityChanged;
return this;
}
public IdentityHeaderBuilder setSignature(String signature) {
this.signature = signature;
return this;
}
public IdentityHeaderBuilder setSignatureChanged(boolean signatureChanged) {
this.signatureChanged = signatureChanged;
return this;
}
public IdentityHeaderBuilder setMessageReference(MessageReference messageReference) {
this.messageReference = messageReference;
return this;
}
public IdentityHeaderBuilder setBody(TextBody body) {
this.body = body;
return this;
}
public IdentityHeaderBuilder setBodyPlain(TextBody bodyPlain) {
this.bodyPlain = bodyPlain;
return this;
}
public IdentityHeaderBuilder setCursorPosition(int cursorPosition) {
this.cursorPosition = cursorPosition;
return this;
}
}

View File

@ -0,0 +1,94 @@
package com.fsck.k9.message;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import android.net.Uri;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.filter.Base64;
public class IdentityHeaderParser {
/**
* Parse an identity string. Handles both legacy and new (!) style identities.
*
* @param identityString
* The encoded identity string that was saved in a drafts header.
*
* @return A map containing the value for each {@link IdentityField} in the identity string.
*/
public static Map<IdentityField, String> parse(final String identityString) {
Map<IdentityField, String> identity = new HashMap<IdentityField, String>();
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Decoding identity: " + identityString);
}
if (identityString == null || identityString.length() < 1) {
return identity;
}
// Check to see if this is a "next gen" identity.
if (identityString.charAt(0) == IdentityField.IDENTITY_VERSION_1.charAt(0) && identityString.length() > 2) {
Uri.Builder builder = new Uri.Builder();
builder.encodedQuery(identityString.substring(1)); // Need to cut off the ! at the beginning.
Uri uri = builder.build();
for (IdentityField key : IdentityField.values()) {
String value = uri.getQueryParameter(key.value());
if (value != null) {
identity.put(key, value);
}
}
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Decoded identity: " + identity.toString());
}
// Sanity check our Integers so that recipients of this result don't have to.
for (IdentityField key : IdentityField.getIntegerFields()) {
if (identity.get(key) != null) {
try {
Integer.parseInt(identity.get(key));
} catch (NumberFormatException e) {
Log.e(K9.LOG_TAG, "Invalid " + key.name() + " field in identity: " + identity.get(key));
}
}
}
} else {
// Legacy identity
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Got a saved legacy identity: " + identityString);
}
StringTokenizer tokenizer = new StringTokenizer(identityString, ":", false);
// First item is the body length. We use this to separate the composed reply from the quoted text.
if (tokenizer.hasMoreTokens()) {
String bodyLengthS = Base64.decode(tokenizer.nextToken());
try {
identity.put(IdentityField.LENGTH, Integer.valueOf(bodyLengthS).toString());
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Unable to parse bodyLength '" + bodyLengthS + "'");
}
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.SIGNATURE, Base64.decode(tokenizer.nextToken()));
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.NAME, Base64.decode(tokenizer.nextToken()));
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.EMAIL, Base64.decode(tokenizer.nextToken()));
}
if (tokenizer.hasMoreTokens()) {
identity.put(IdentityField.QUOTED_TEXT_MODE, Base64.decode(tokenizer.nextToken()));
}
}
return identity;
}
}

View File

@ -1,4 +1,4 @@
package com.fsck.k9.activity;
package com.fsck.k9.message;
import java.io.Serializable;
@ -12,7 +12,7 @@ import java.io.Serializable;
*
* TODO: This container should also have a text part, along with its insertion point. Or maybe a generic InsertableContent and maintain one each for Html and Text?
*/
class InsertableHtmlContent implements Serializable {
public class InsertableHtmlContent implements Serializable {
private static final long serialVersionUID = 2397327034L;
// Default to a headerInsertionPoint at the beginning of the message.
private int headerInsertionPoint = 0;

View File

@ -0,0 +1,450 @@
package com.fsck.k9.message;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import android.content.Context;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.activity.misc.Attachment;
import com.fsck.k9.crypto.PgpData;
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.MimeBodyPart;
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.TempFileBody;
import com.fsck.k9.mailstore.TempFileMessageBody;
import org.apache.james.mime4j.codec.EncoderUtil;
import org.apache.james.mime4j.util.MimeUtil;
public class MessageBuilder {
private final Context context;
private String subject;
private Address[] to;
private Address[] cc;
private Address[] bcc;
private String inReplyTo;
private String references;
private boolean requestReadReceipt;
private Identity identity;
private SimpleMessageFormat messageFormat;
private String text;
private PgpData pgpData;
private List<Attachment> attachments;
private String signature;
private QuoteStyle quoteStyle;
private QuotedTextMode quotedTextMode;
private String quotedText;
private InsertableHtmlContent quotedHtmlContent;
private boolean isReplyAfterQuote;
private boolean isSignatureBeforeQuotedText;
private boolean identityChanged;
private boolean signatureChanged;
private int cursorPosition;
private MessageReference messageReference;
private boolean isDraft;
public MessageBuilder(Context context) {
this.context = context;
}
/**
* Build the final message to be sent (or saved). If there is another message quoted in this one, it will be baked
* into the final message here.
*/
public MimeMessage build() throws MessagingException {
//FIXME: check arguments
MimeMessage message = new MimeMessage();
buildHeader(message);
buildBody(message);
return message;
}
private void buildHeader(MimeMessage message) throws MessagingException {
message.addSentDate(new Date(), K9.hideTimeZone());
Address from = new Address(identity.getEmail(), identity.getName());
message.setFrom(from);
message.setRecipients(RecipientType.TO, to);
message.setRecipients(RecipientType.CC, cc);
message.setRecipients(RecipientType.BCC, bcc);
message.setSubject(subject);
if (requestReadReceipt) {
message.setHeader("Disposition-Notification-To", from.toEncodedString());
message.setHeader("X-Confirm-Reading-To", from.toEncodedString());
message.setHeader("Return-Receipt-To", from.toEncodedString());
}
if (!K9.hideUserAgent()) {
message.setHeader("User-Agent", context.getString(R.string.message_header_mua));
}
final String replyTo = identity.getReplyTo();
if (replyTo != null) {
message.setReplyTo(new Address[] { new Address(replyTo) });
}
if (inReplyTo != null) {
message.setInReplyTo(inReplyTo);
}
if (references != null) {
message.setReferences(references);
}
message.generateMessageId();
}
private void buildBody(MimeMessage message) throws MessagingException {
// Build the body.
// TODO FIXME - body can be either an HTML or Text part, depending on whether we're in
// HTML mode or not. Should probably fix this so we don't mix up html and text parts.
TextBody body;
if (pgpData.getEncryptedData() != null) {
String text = pgpData.getEncryptedData();
body = new TextBody(text);
} else {
body = buildText(isDraft);
}
// text/plain part when messageFormat == MessageFormat.HTML
TextBody bodyPlain = null;
final boolean hasAttachments = !attachments.isEmpty();
if (messageFormat == SimpleMessageFormat.HTML) {
// HTML message (with alternative text part)
// This is the compiled MIME part for an HTML message.
MimeMultipart composedMimeMessage = new MimeMultipart();
composedMimeMessage.setSubType("alternative"); // Let the receiver select either the text or the HTML part.
composedMimeMessage.addBodyPart(new MimeBodyPart(body, "text/html"));
bodyPlain = buildText(isDraft, SimpleMessageFormat.TEXT);
composedMimeMessage.addBodyPart(new MimeBodyPart(bodyPlain, "text/plain"));
if (hasAttachments) {
// If we're HTML and have attachments, we have a MimeMultipart container to hold the
// whole message (mp here), of which one part is a MimeMultipart container
// (composedMimeMessage) with the user's composed messages, and subsequent parts for
// the attachments.
MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(composedMimeMessage));
addAttachmentsToMessage(mp);
MimeMessageHelper.setBody(message, mp);
} else {
// If no attachments, our multipart/alternative part is the only one we need.
MimeMessageHelper.setBody(message, composedMimeMessage);
}
} else if (messageFormat == SimpleMessageFormat.TEXT) {
// Text-only message.
if (hasAttachments) {
MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(body, "text/plain"));
addAttachmentsToMessage(mp);
MimeMessageHelper.setBody(message, mp);
} else {
// No attachments to include, just stick the text body in the message and call it good.
MimeMessageHelper.setBody(message, body);
}
}
// If this is a draft, add metadata for thawing.
if (isDraft) {
// Add the identity to the message.
message.addHeader(K9.IDENTITY_HEADER, buildIdentityHeader(body, bodyPlain));
}
}
public TextBody buildText() {
return buildText(isDraft, messageFormat);
}
private String buildIdentityHeader(TextBody body, TextBody bodyPlain) {
return new IdentityHeaderBuilder()
.setCursorPosition(cursorPosition)
.setIdentity(identity)
.setIdentityChanged(identityChanged)
.setMessageFormat(messageFormat)
.setMessageReference(messageReference)
.setQuotedHtmlContent(quotedHtmlContent)
.setQuoteStyle(quoteStyle)
.setQuoteTextMode(quotedTextMode)
.setSignature(signature)
.setSignatureChanged(signatureChanged)
.setBody(body)
.setBodyPlain(bodyPlain)
.build();
}
/**
* Add attachments as parts into a MimeMultipart container.
* @param mp MimeMultipart container in which to insert parts.
* @throws MessagingException
*/
private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException {
Body body;
for (Attachment attachment : attachments) {
if (attachment.state != Attachment.LoadingState.COMPLETE) {
continue;
}
String contentType = attachment.contentType;
if (MimeUtil.isMessage(contentType)) {
body = new TempFileMessageBody(attachment.filename);
} else {
body = new TempFileBody(attachment.filename);
}
MimeBodyPart bp = new MimeBodyPart(body);
/*
* Correctly encode the filename here. Otherwise the whole
* header value (all parameters at once) will be encoded by
* MimeHeader.writeTo().
*/
bp.addHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n name=\"%s\"",
contentType,
EncoderUtil.encodeIfNecessary(attachment.name,
EncoderUtil.Usage.WORD_ENTITY, 7)));
bp.setEncoding(MimeUtility.getEncodingforType(contentType));
/*
* TODO: Oh the joys of MIME...
*
* From RFC 2183 (The Content-Disposition Header Field):
* "Parameter values longer than 78 characters, or which
* contain non-ASCII characters, MUST be encoded as specified
* in [RFC 2184]."
*
* Example:
*
* Content-Type: application/x-stuff
* title*1*=us-ascii'en'This%20is%20even%20more%20
* title*2*=%2A%2A%2Afun%2A%2A%2A%20
* title*3="isn't it!"
*/
bp.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, String.format(Locale.US,
"attachment;\r\n filename=\"%s\";\r\n size=%d",
attachment.name, attachment.size));
mp.addBodyPart(bp);
}
}
/**
* Build the Body that will contain the text of the message. We'll decide where to
* include it later. Draft messages are treated somewhat differently in that signatures are not
* appended and HTML separators between composed text and quoted text are not added.
* @param isDraft If we should build a message that will be saved as a draft (as opposed to sent).
*/
private TextBody buildText(boolean isDraft) {
return buildText(isDraft, messageFormat);
}
/**
* Build the {@link Body} that will contain the text of the message.
*
* <p>
* Draft messages are treated somewhat differently in that signatures are not appended and HTML
* separators between composed text and quoted text are not added.
* </p>
*
* @param isDraft
* If {@code true} we build a message that will be saved as a draft (as opposed to
* sent).
* @param simpleMessageFormat
* Specifies what type of message to build ({@code text/plain} vs. {@code text/html}).
*
* @return {@link TextBody} instance that contains the entered text and possibly the quoted
* original message.
*/
private TextBody buildText(boolean isDraft, SimpleMessageFormat simpleMessageFormat) {
String messageText = text;
TextBodyBuilder textBodyBuilder = new TextBodyBuilder(messageText);
/*
* Find out if we need to include the original message as quoted text.
*
* We include the quoted text in the body if the user didn't choose to
* hide it. We always include the quoted text when we're saving a draft.
* That's so the user is able to "un-hide" the quoted text if (s)he
* opens a saved draft.
*/
boolean includeQuotedText = (isDraft || quotedTextMode == QuotedTextMode.SHOW);
boolean isReplyAfterQuote = (quoteStyle == QuoteStyle.PREFIX && this.isReplyAfterQuote);
textBodyBuilder.setIncludeQuotedText(false);
if (includeQuotedText) {
if (simpleMessageFormat == SimpleMessageFormat.HTML && quotedHtmlContent != null) {
textBodyBuilder.setIncludeQuotedText(true);
textBodyBuilder.setQuotedTextHtml(quotedHtmlContent);
textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote);
}
if (simpleMessageFormat == SimpleMessageFormat.TEXT && quotedText.length() > 0) {
textBodyBuilder.setIncludeQuotedText(true);
textBodyBuilder.setQuotedText(quotedText);
textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote);
}
}
textBodyBuilder.setInsertSeparator(!isDraft);
boolean useSignature = (!isDraft && identity.getSignatureUse());
if (useSignature) {
textBodyBuilder.setAppendSignature(true);
textBodyBuilder.setSignature(signature);
textBodyBuilder.setSignatureBeforeQuotedText(isSignatureBeforeQuotedText);
} else {
textBodyBuilder.setAppendSignature(false);
}
TextBody body;
if (simpleMessageFormat == SimpleMessageFormat.HTML) {
body = textBodyBuilder.buildTextHtml();
} else {
body = textBodyBuilder.buildTextPlain();
}
return body;
}
public MessageBuilder setSubject(String subject) {
this.subject = subject;
return this;
}
public MessageBuilder setTo(Address[] to) {
this.to = to;
return this;
}
public MessageBuilder setCc(Address[] cc) {
this.cc = cc;
return this;
}
public MessageBuilder setBcc(Address[] bcc) {
this.bcc = bcc;
return this;
}
public MessageBuilder setInReplyTo(String inReplyTo) {
this.inReplyTo = inReplyTo;
return this;
}
public MessageBuilder setReferences(String references) {
this.references = references;
return this;
}
public MessageBuilder setRequestReadReceipt(boolean requestReadReceipt) {
this.requestReadReceipt = requestReadReceipt;
return this;
}
public MessageBuilder setIdentity(Identity identity) {
this.identity = identity;
return this;
}
public MessageBuilder setMessageFormat(SimpleMessageFormat messageFormat) {
this.messageFormat = messageFormat;
return this;
}
public MessageBuilder setText(String text) {
this.text = text;
return this;
}
public MessageBuilder setPgpData(PgpData pgpData) {
this.pgpData = pgpData;
return this;
}
public MessageBuilder setAttachments(List<Attachment> attachments) {
this.attachments = attachments;
return this;
}
public MessageBuilder setSignature(String signature) {
this.signature = signature;
return this;
}
public MessageBuilder setQuoteStyle(QuoteStyle quoteStyle) {
this.quoteStyle = quoteStyle;
return this;
}
public MessageBuilder setQuotedTextMode(QuotedTextMode quotedTextMode) {
this.quotedTextMode = quotedTextMode;
return this;
}
public MessageBuilder setQuotedText(String quotedText) {
this.quotedText = quotedText;
return this;
}
public MessageBuilder setQuotedHtmlContent(InsertableHtmlContent quotedHtmlContent) {
this.quotedHtmlContent = quotedHtmlContent;
return this;
}
public MessageBuilder setReplyAfterQuote(boolean isReplyAfterQuote) {
this.isReplyAfterQuote = isReplyAfterQuote;
return this;
}
public MessageBuilder setSignatureBeforeQuotedText(boolean isSignatureBeforeQuotedText) {
this.isSignatureBeforeQuotedText = isSignatureBeforeQuotedText;
return this;
}
public MessageBuilder setIdentityChanged(boolean identityChanged) {
this.identityChanged = identityChanged;
return this;
}
public MessageBuilder setSignatureChanged(boolean signatureChanged) {
this.signatureChanged = signatureChanged;
return this;
}
public MessageBuilder setCursorPosition(int cursorPosition) {
this.cursorPosition = cursorPosition;
return this;
}
public MessageBuilder setMessageReference(MessageReference messageReference) {
this.messageReference = messageReference;
return this;
}
public MessageBuilder setDraft(boolean isDraft) {
this.isDraft = isDraft;
return this;
}
}

View File

@ -0,0 +1,8 @@
package com.fsck.k9.message;
public enum QuotedTextMode {
NONE,
SHOW,
HIDE
}

View File

@ -0,0 +1,7 @@
package com.fsck.k9.message;
public enum SimpleMessageFormat {
TEXT,
HTML
}

View File

@ -1,4 +1,4 @@
package com.fsck.k9.activity;
package com.fsck.k9.message;
import android.text.TextUtils;
import android.util.Log;

View File

@ -1,15 +1,19 @@
package com.fsck.k9.provider;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.BuildConfig;
import com.fsck.k9.K9;
@ -18,30 +22,19 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStore.AttachmentInfo;
import com.fsck.k9.mailstore.StorageManager;
import org.openintents.openpgp.util.ParcelFileDescriptorUtil;
import java.io.*;
import java.util.List;
/**
* A simple ContentProvider that allows file access to attachments.
*
* <p>
* Warning! We make heavy assumptions about the Uris used by the {@link LocalStore} for an
* {@link Account} here.
* </p>
*/
public class AttachmentProvider extends ContentProvider {
private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".attachmentprovider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
private static final String FORMAT_RAW = "RAW";
private static final String FORMAT_VIEW = "VIEW";
private static final String FORMAT_THUMBNAIL = "THUMBNAIL";
private static final String[] DEFAULT_PROJECTION = new String[] {
AttachmentProviderColumns._ID,
AttachmentProviderColumns.DATA,
AttachmentProviderColumns._ID,
AttachmentProviderColumns.DATA,
};
public static class AttachmentProviderColumns {
@ -52,171 +45,49 @@ public class AttachmentProvider extends ContentProvider {
}
public static Uri getAttachmentUri(Account account, long id) {
public static Uri getAttachmentUri(String accountUuid, long id) {
return CONTENT_URI.buildUpon()
.appendPath(account.getUuid())
.appendPath(accountUuid)
.appendPath(Long.toString(id))
.appendPath(FORMAT_RAW)
.build();
}
public static Uri getAttachmentUriForViewing(Account account, long id, String mimeType, String filename) {
return CONTENT_URI.buildUpon()
.appendPath(account.getUuid())
.appendPath(Long.toString(id))
.appendPath(FORMAT_VIEW)
.appendPath(mimeType)
.appendPath(filename)
.build();
}
public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) {
return CONTENT_URI.buildUpon()
.appendPath(account.getUuid())
.appendPath(Long.toString(id))
.appendPath(FORMAT_THUMBNAIL)
.appendPath(Integer.toString(width))
.appendPath(Integer.toString(height))
.build();
}
public static void clear(Context context) {
/*
* We use the cache dir as a temporary directory (since Android doesn't give us one) so
* on startup we'll clean up any .tmp files from the last run.
*/
File[] files = context.getCacheDir().listFiles();
for (File file : files) {
try {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Deleting file " + file.getCanonicalPath());
}
} catch (IOException ioe) { /* No need to log failure to log */ }
file.delete();
}
}
/**
* Delete the thumbnail of an attachment.
*
* @param context
* The application context.
* @param accountUuid
* The UUID of the account the attachment belongs to.
* @param attachmentId
* The ID of the attachment the thumbnail was created for.
*/
public static void deleteThumbnail(Context context, String accountUuid, String attachmentId) {
File file = getThumbnailFile(context, accountUuid, attachmentId);
if (file.exists()) {
file.delete();
}
}
private static File getThumbnailFile(Context context, String accountUuid,
String attachmentId) {
String filename = "thmb_" + accountUuid + "_" + attachmentId + ".tmp";
File dir = context.getCacheDir();
return new File(dir, filename);
}
@Override
public boolean onCreate() {
/*
* We use the cache dir as a temporary directory (since Android doesn't give us one) so
* on startup we'll clean up any .tmp files from the last run.
*/
final File cacheDir = getContext().getCacheDir();
if (cacheDir == null) {
return true;
}
File[] files = cacheDir.listFiles();
if (files == null) {
return true;
}
for (File file : files) {
if (file.getName().endsWith(".tmp")) {
file.delete();
}
}
return true;
}
@Override
public String getType(Uri uri) {
List<String> segments = uri.getPathSegments();
String dbName = segments.get(0);
String accountUuid = segments.get(0);
String id = segments.get(1);
String format = segments.get(2);
String mimeType = (segments.size() < 4) ? null : segments.get(3);
String mimeType = (segments.size() < 3) ? null : segments.get(2);
return getType(dbName, id, format, mimeType);
return getType(accountUuid, id, mimeType);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File file;
List<String> segments = uri.getPathSegments();
String accountUuid = segments.get(0);
String attachmentId = segments.get(1);
String format = segments.get(2);
if (FORMAT_THUMBNAIL.equals(format)) {
int width = Integer.parseInt(segments.get(3));
int height = Integer.parseInt(segments.get(4));
file = getThumbnailFile(getContext(), accountUuid, attachmentId);
if (!file.exists()) {
String type = getType(accountUuid, attachmentId, FORMAT_VIEW, null);
try {
FileInputStream in = new FileInputStream(getFile(accountUuid, attachmentId));
try {
Bitmap thumbnail = createThumbnail(type, in);
if (thumbnail != null) {
thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
FileOutputStream out = new FileOutputStream(file);
try {
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
} finally {
out.close();
}
}
} finally {
try { in.close(); } catch (Throwable ignore) { /* ignore */ }
}
} catch (IOException ioe) {
return null;
}
}
} else {
file = getFile(accountUuid, attachmentId);
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return openAttachment(accountUuid, attachmentId);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection;
List<String> segments = uri.getPathSegments();
String dbName = segments.get(0);
String accountUuid = segments.get(0);
String id = segments.get(1);
// Versions of K-9 before 3.400 had a database name here, not an
// account UID, so implement a bit of backcompat
if (dbName.endsWith(".db")) {
dbName = dbName.substring(0, dbName.length() - 3);
}
final AttachmentInfo attachmentInfo;
try {
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid);
attachmentInfo = LocalStore.getInstance(account, getContext()).getAttachmentInfo(id);
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Unable to retrieve attachment info from local store for ID: " + id, e);
@ -263,66 +134,43 @@ public class AttachmentProvider extends ContentProvider {
return null;
}
private String getType(String dbName, String id, String format, String mimeType) {
private String getType(String accountUuid, String id, String mimeType) {
String type;
if (FORMAT_THUMBNAIL.equals(format)) {
type = "image/png";
} else {
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid);
try {
final LocalStore localStore = LocalStore.getInstance(account, getContext());
try {
final LocalStore localStore = LocalStore.getInstance(account, getContext());
AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id);
if (FORMAT_VIEW.equals(format) && mimeType != null) {
type = mimeType;
} else {
type = attachmentInfo.type;
}
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e);
type = null;
AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id);
if (mimeType != null) {
type = mimeType;
} else {
type = attachmentInfo.type;
}
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e);
type = MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE;
}
return type;
}
private File getFile(String dbName, String id) throws FileNotFoundException {
Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
File attachmentsDir = StorageManager.getInstance(getContext()).getAttachmentDirectory(dbName,
account.getLocalStorageProviderId());
File file = new File(attachmentsDir, id);
if (!file.exists()) {
throw new FileNotFoundException(file.getAbsolutePath());
}
return file;
}
private Bitmap createThumbnail(String type, InputStream data) {
if (MimeUtility.mimeTypeMatches(type, "image/*")) {
return createImageThumbnail(data);
}
return null;
}
private Bitmap createImageThumbnail(InputStream data) {
private ParcelFileDescriptor openAttachment(String accountUuid, String attachmentId) {
try {
Bitmap bitmap = BitmapFactory.decodeStream(data);
return bitmap;
} catch (OutOfMemoryError oome) {
/*
* Improperly downloaded images, corrupt bitmaps and the like can commonly
* cause OOME due to invalid allocation sizes. We're happy with a null bitmap in
* that case. If the system is really out of memory we'll know about it soon
* enough.
*/
InputStream inputStream = getAttachmentInputStream(accountUuid, attachmentId);
return ParcelFileDescriptorUtil.pipeFrom(inputStream, null);
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Error getting InputStream for attachment", e);
return null;
} catch (Exception e) {
} catch (IOException e) {
Log.e(K9.LOG_TAG, "Error creating ParcelFileDescriptor", e);
return null;
}
}
private InputStream getAttachmentInputStream(String accountUuid, String attachmentId) throws MessagingException {
final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid);
LocalStore localStore = LocalStore.getInstance(account, getContext());
return localStore.getAttachmentInputStream(attachmentId);
}
}

View File

@ -153,8 +153,6 @@ public class EmailProvider extends ContentProvider {
private interface InternalMessageColumns extends MessageColumns {
public static final String DELETED = "deleted";
public static final String EMPTY = "empty";
public static final String TEXT_CONTENT = "text_content";
public static final String HTML_CONTENT = "html_content";
public static final String MIME_TYPE = "mime_type";
}

View File

@ -0,0 +1,25 @@
package com.fsck.k9.provider;
import java.io.File;
import android.content.Context;
import android.net.Uri;
import android.support.v4.content.FileProvider;
import com.fsck.k9.BuildConfig;
public class K9FileProvider extends FileProvider {
private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider";
public static Uri getUriForFile(Context context, File file, String mimeType) {
Uri uri = FileProvider.getUriForFile(context, AUTHORITY, file);
return uri.buildUpon().appendQueryParameter("mime_type", mimeType).build();
}
@Override
public String getType(Uri uri) {
return uri.getQueryParameter("mime_type");
}
}

View File

@ -140,8 +140,7 @@ public class SqlQueryBuilder {
break;
}
case MESSAGE_CONTENTS: {
columnName = "text_content";
break;
throw new RuntimeException("Searching in message bodies is currently not supported");
}
case REPLY_TO: {
columnName = "reply_to_list";

View File

@ -0,0 +1,37 @@
package com.fsck.k9.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.EditText;
/**
* An {@link android.widget.EditText} extension with methods that convert line endings from
* {@code \r\n} to {@code \n} and back again when setting and getting text.
*
*/
public class EolConvertingEditText extends EditText {
public EolConvertingEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Return the text the EolConvertingEditText is displaying.
*
* @return A string with any line endings converted to {@code \r\n}.
*/
public String getCharacters() {
return getText().toString().replace("\n", "\r\n");
}
/**
* Sets the string value of the EolConvertingEditText. Any line endings
* in the string will be converted to {@code \n}.
*
* @param text
*/
public void setCharacters(CharSequence text) {
setText(text.toString().replace("\r\n", "\n"));
}
}

View File

@ -0,0 +1,28 @@
package com.fsck.k9.ui.crypto;
import java.util.HashMap;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mailstore.OpenPgpResultAnnotation;
public class MessageCryptoAnnotations {
private HashMap<Part, OpenPgpResultAnnotation> annotations = new HashMap<Part, OpenPgpResultAnnotation>();
MessageCryptoAnnotations() {
// Package-private constructor
}
void put(Part part, OpenPgpResultAnnotation annotation) {
annotations.put(part, annotation);
}
public OpenPgpResultAnnotation get(Part part) {
return annotations.get(part);
}
public boolean has(Part part) {
return annotations.containsKey(part);
}
}

View File

@ -0,0 +1,6 @@
package com.fsck.k9.ui.crypto;
public interface MessageCryptoCallback {
void onCryptoOperationsFinished(MessageCryptoAnnotations annotations);
}

View File

@ -0,0 +1,478 @@
package com.fsck.k9.ui.crypto;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.os.AsyncTask;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.crypto.MessageDecryptVerifier;
import com.fsck.k9.crypto.OpenPgpApiHelper;
import com.fsck.k9.helper.IdentityHelper;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.DecryptStreamParser;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.MessageHelper;
import com.fsck.k9.mailstore.OpenPgpResultAnnotation;
import com.fsck.k9.mailstore.OpenPgpResultAnnotation.CryptoError;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import org.openintents.openpgp.util.OpenPgpServiceConnection.OnBound;
public class MessageCryptoHelper {
private static final int REQUEST_CODE_CRYPTO = 1000;
private static final int INVALID_OPENPGP_RESULT_CODE = -1;
private static final MimeBodyPart NO_REPLACEMENT_PART = null;
private final Context context;
private final Activity activity;
private final MessageCryptoCallback callback;
private final Account account;
private LocalMessage message;
private Deque<CryptoPart> partsToDecryptOrVerify = new ArrayDeque<CryptoPart>();
private OpenPgpApi openPgpApi;
private CryptoPart currentCryptoPart;
private Intent currentCryptoResult;
private MessageCryptoAnnotations messageAnnotations;
public MessageCryptoHelper(Activity activity, Account account, MessageCryptoCallback callback) {
this.context = activity.getApplicationContext();
this.activity = activity;
this.callback = callback;
this.account = account;
this.messageAnnotations = new MessageCryptoAnnotations();
}
public void decryptOrVerifyMessagePartsIfNecessary(LocalMessage message) {
this.message = message;
if (!account.isOpenPgpProviderConfigured()) {
returnResultToFragment();
return;
}
List<Part> encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
processFoundParts(encryptedParts, CryptoPartType.ENCRYPTED, CryptoError.ENCRYPTED_BUT_INCOMPLETE,
MessageHelper.createEmptyPart());
List<Part> signedParts = MessageDecryptVerifier.findSignedParts(message);
processFoundParts(signedParts, CryptoPartType.SIGNED, CryptoError.SIGNED_BUT_INCOMPLETE, NO_REPLACEMENT_PART);
List<Part> inlineParts = MessageDecryptVerifier.findPgpInlineParts(message);
addFoundInlinePgpParts(inlineParts);
decryptOrVerifyNextPart();
}
private void processFoundParts(List<Part> foundParts, CryptoPartType cryptoPartType, CryptoError errorIfIncomplete,
MimeBodyPart replacementPart) {
for (Part part : foundParts) {
if (MessageHelper.isCompletePartAvailable(part)) {
CryptoPart cryptoPart = new CryptoPart(cryptoPartType, part);
partsToDecryptOrVerify.add(cryptoPart);
} else {
addErrorAnnotation(part, errorIfIncomplete, replacementPart);
}
}
}
private void addErrorAnnotation(Part part, CryptoError error, MimeBodyPart outputData) {
OpenPgpResultAnnotation annotation = new OpenPgpResultAnnotation();
annotation.setErrorType(error);
annotation.setOutputData(outputData);
messageAnnotations.put(part, annotation);
}
private void addFoundInlinePgpParts(List<Part> foundParts) {
for (Part part : foundParts) {
CryptoPart cryptoPart = new CryptoPart(CryptoPartType.INLINE_PGP, part);
partsToDecryptOrVerify.add(cryptoPart);
}
}
private void decryptOrVerifyNextPart() {
if (partsToDecryptOrVerify.isEmpty()) {
returnResultToFragment();
return;
}
CryptoPart cryptoPart = partsToDecryptOrVerify.peekFirst();
startDecryptingOrVerifyingPart(cryptoPart);
}
private void startDecryptingOrVerifyingPart(CryptoPart cryptoPart) {
if (!isBoundToCryptoProviderService()) {
connectToCryptoProviderService();
} else {
decryptOrVerifyPart(cryptoPart);
}
}
private boolean isBoundToCryptoProviderService() {
return openPgpApi != null;
}
private void connectToCryptoProviderService() {
String openPgpProvider = account.getOpenPgpProvider();
new OpenPgpServiceConnection(context, openPgpProvider,
new OnBound() {
@Override
public void onBound(IOpenPgpService service) {
openPgpApi = new OpenPgpApi(context, service);
decryptOrVerifyNextPart();
}
@Override
public void onError(Exception e) {
Log.e(K9.LOG_TAG, "Couldn't connect to OpenPgpService", e);
}
}).bindToService();
}
private void decryptOrVerifyPart(CryptoPart cryptoPart) {
currentCryptoPart = cryptoPart;
decryptVerify(new Intent());
}
private void decryptVerify(Intent intent) {
intent.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
Identity identity = IdentityHelper.getRecipientIdentityFromMessage(account, message);
String accountName = OpenPgpApiHelper.buildAccountName(identity);
intent.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, accountName);
try {
CryptoPartType cryptoPartType = currentCryptoPart.type;
switch (cryptoPartType) {
case SIGNED: {
callAsyncDetachedVerify(intent);
return;
}
case ENCRYPTED: {
callAsyncDecrypt(intent);
return;
}
case INLINE_PGP: {
callAsyncInlineOperation(intent);
return;
}
}
throw new IllegalStateException("Unknown crypto part type: " + cryptoPartType);
} catch (IOException e) {
Log.e(K9.LOG_TAG, "IOException", e);
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "MessagingException", e);
}
}
private void callAsyncInlineOperation(Intent intent) throws IOException {
PipedInputStream pipedInputStream = getPipedInputStreamForEncryptedOrInlineData();
final ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream();
openPgpApi.executeApiAsync(intent, pipedInputStream, decryptedOutputStream, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
currentCryptoResult = result;
MimeBodyPart decryptedPart = null;
try {
TextBody body = new TextBody(new String(decryptedOutputStream.toByteArray()));
decryptedPart = new MimeBodyPart(body, "text/plain");
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "MessagingException", e);
}
onCryptoOperationReturned(decryptedPart);
}
});
}
private void callAsyncDecrypt(Intent intent) throws IOException {
final CountDownLatch latch = new CountDownLatch(1);
PipedInputStream pipedInputStream = getPipedInputStreamForEncryptedOrInlineData();
PipedOutputStream decryptedOutputStream = getPipedOutputStreamForDecryptedData(latch);
openPgpApi.executeApiAsync(intent, pipedInputStream, decryptedOutputStream, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
currentCryptoResult = result;
latch.countDown();
}
});
}
private void callAsyncDetachedVerify(Intent intent) throws IOException, MessagingException {
PipedInputStream pipedInputStream = getPipedInputStreamForSignedData();
byte[] signatureData = MessageDecryptVerifier.getSignatureData(currentCryptoPart.part);
intent.putExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE, signatureData);
openPgpApi.executeApiAsync(intent, pipedInputStream, null, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
currentCryptoResult = result;
onCryptoOperationReturned(null);
}
});
}
private PipedInputStream getPipedInputStreamForSignedData() throws IOException {
PipedInputStream pipedInputStream = new PipedInputStream();
final PipedOutputStream out = new PipedOutputStream(pipedInputStream);
new Thread(new Runnable() {
@Override
public void run() {
try {
Multipart multipartSignedMultipart = (Multipart) currentCryptoPart.part.getBody();
BodyPart signatureBodyPart = multipartSignedMultipart.getBodyPart(0);
Log.d(K9.LOG_TAG, "signed data type: " + signatureBodyPart.getMimeType());
signatureBodyPart.writeTo(out);
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Exception while writing message to crypto provider", e);
} finally {
try {
out.close();
} catch (IOException e) {
// don't care
}
}
}
}).start();
return pipedInputStream;
}
private PipedInputStream getPipedInputStreamForEncryptedOrInlineData() throws IOException {
PipedInputStream pipedInputStream = new PipedInputStream();
final PipedOutputStream out = new PipedOutputStream(pipedInputStream);
new Thread(new Runnable() {
@Override
public void run() {
try {
Part part = currentCryptoPart.part;
CryptoPartType cryptoPartType = currentCryptoPart.type;
if (cryptoPartType == CryptoPartType.ENCRYPTED) {
Multipart multipartEncryptedMultipart = (Multipart) part.getBody();
BodyPart encryptionPayloadPart = multipartEncryptedMultipart.getBodyPart(1);
Body encryptionPayloadBody = encryptionPayloadPart.getBody();
encryptionPayloadBody.writeTo(out);
} else if (cryptoPartType == CryptoPartType.INLINE_PGP) {
String text = MessageExtractor.getTextFromPart(part);
out.write(text.getBytes());
} else {
Log.wtf(K9.LOG_TAG, "No suitable data to stream found!");
}
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Exception while writing message to crypto provider", e);
} finally {
try {
out.close();
} catch (IOException e) {
// don't care
}
}
}
}).start();
return pipedInputStream;
}
private PipedOutputStream getPipedOutputStreamForDecryptedData(final CountDownLatch latch) throws IOException {
PipedOutputStream decryptedOutputStream = new PipedOutputStream();
final PipedInputStream decryptedInputStream = new PipedInputStream(decryptedOutputStream);
new AsyncTask<Void, Void, MimeBodyPart>() {
@Override
protected MimeBodyPart doInBackground(Void... params) {
MimeBodyPart decryptedPart = null;
try {
decryptedPart = DecryptStreamParser.parse(context, decryptedInputStream);
latch.await();
} catch (InterruptedException e) {
Log.w(K9.LOG_TAG, "we were interrupted while waiting for onReturn!", e);
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Something went wrong while parsing the decrypted MIME part", e);
//TODO: pass error to main thread and display error message to user
}
return decryptedPart;
}
@Override
protected void onPostExecute(MimeBodyPart decryptedPart) {
onCryptoOperationReturned(decryptedPart);
}
}.execute();
return decryptedOutputStream;
}
private void onCryptoOperationReturned(MimeBodyPart outputPart) {
if (currentCryptoResult == null) {
Log.e(K9.LOG_TAG, "Internal error: we should have a result here!");
return;
}
try {
handleCryptoOperationResult(outputPart);
} finally {
currentCryptoResult = null;
}
}
private void handleCryptoOperationResult(MimeBodyPart outputPart) {
int resultCode = currentCryptoResult.getIntExtra(OpenPgpApi.RESULT_CODE, INVALID_OPENPGP_RESULT_CODE);
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "OpenPGP API decryptVerify result code: " + resultCode);
}
switch (resultCode) {
case INVALID_OPENPGP_RESULT_CODE: {
Log.e(K9.LOG_TAG, "Internal error: no result code!");
break;
}
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
handleUserInteractionRequest();
break;
}
case OpenPgpApi.RESULT_CODE_ERROR: {
handleCryptoOperationError();
break;
}
case OpenPgpApi.RESULT_CODE_SUCCESS: {
handleCryptoOperationSuccess(outputPart);
break;
}
}
}
private void handleUserInteractionRequest() {
PendingIntent pendingIntent = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
if (pendingIntent == null) {
throw new AssertionError("Expecting PendingIntent on USER_INTERACTION_REQUIRED!");
}
try {
activity.startIntentSenderForResult(pendingIntent.getIntentSender(), REQUEST_CODE_CRYPTO, null, 0, 0, 0);
} catch (SendIntentException e) {
Log.e(K9.LOG_TAG, "Internal error on starting pendingintent!", e);
}
}
private void handleCryptoOperationError() {
OpenPgpError error = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
if (K9.DEBUG) {
Log.w(K9.LOG_TAG, "OpenPGP API error: " + error.getMessage());
}
onCryptoFailed(error);
}
private void handleCryptoOperationSuccess(MimeBodyPart outputPart) {
OpenPgpResultAnnotation resultAnnotation = new OpenPgpResultAnnotation();
resultAnnotation.setOutputData(outputPart);
// TODO if the data /was/ encrypted, we should set it here!
// this is not easy to determine for inline data though
resultAnnotation.setWasEncrypted(false);
OpenPgpSignatureResult signatureResult = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
resultAnnotation.setSignatureResult(signatureResult);
PendingIntent pendingIntent = currentCryptoResult.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
resultAnnotation.setPendingIntent(pendingIntent);
onCryptoSuccess(resultAnnotation);
}
public void handleCryptoResult(int requestCode, int resultCode, Intent data) {
if (requestCode != REQUEST_CODE_CRYPTO) {
return;
}
if (resultCode == Activity.RESULT_OK) {
decryptOrVerifyNextPart();
} else {
// FIXME: don't pass null
onCryptoFailed(null);
}
}
private void onCryptoSuccess(OpenPgpResultAnnotation resultAnnotation) {
addOpenPgpResultPartToMessage(resultAnnotation);
onCryptoFinished();
}
private void addOpenPgpResultPartToMessage(OpenPgpResultAnnotation resultAnnotation) {
Part part = currentCryptoPart.part;
messageAnnotations.put(part, resultAnnotation);
}
private void onCryptoFailed(OpenPgpError error) {
OpenPgpResultAnnotation errorPart = new OpenPgpResultAnnotation();
errorPart.setError(error);
addOpenPgpResultPartToMessage(errorPart);
onCryptoFinished();
}
private void onCryptoFinished() {
partsToDecryptOrVerify.removeFirst();
decryptOrVerifyNextPart();
}
private void returnResultToFragment() {
callback.onCryptoOperationsFinished(messageAnnotations);
}
private static class CryptoPart {
public final CryptoPartType type;
public final Part part;
CryptoPart(CryptoPartType type, Part part) {
this.type = type;
this.part = part;
}
}
private enum CryptoPartType {
INLINE_PGP,
ENCRYPTED,
SIGNED
}
}

View File

@ -0,0 +1,52 @@
package com.fsck.k9.ui.message;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mailstore.LocalMessageExtractor;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
public class DecodeMessageLoader extends AsyncTaskLoader<MessageViewInfo> {
private final Message message;
private MessageViewInfo messageViewInfo;
private MessageCryptoAnnotations annotations;
public DecodeMessageLoader(Context context, Message message, MessageCryptoAnnotations annotations) {
super(context);
this.message = message;
this.annotations = annotations;
}
@Override
protected void onStartLoading() {
if (messageViewInfo != null) {
super.deliverResult(messageViewInfo);
}
if (takeContentChanged() || messageViewInfo == null) {
forceLoad();
}
}
@Override
public void deliverResult(MessageViewInfo messageViewInfo) {
this.messageViewInfo = messageViewInfo;
super.deliverResult(messageViewInfo);
}
@Override
public MessageViewInfo loadInBackground() {
try {
return LocalMessageExtractor.decodeMessageForView(getContext(), message, annotations);
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Error while decoding message", e);
return null;
}
}
}

View File

@ -0,0 +1,60 @@
package com.fsck.k9.ui.message;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.LocalMessage;
public class LocalMessageLoader extends AsyncTaskLoader<LocalMessage> {
private final MessagingController controller;
private final Account account;
private final MessageReference messageReference;
private LocalMessage message;
public LocalMessageLoader(Context context, MessagingController controller, Account account,
MessageReference messageReference) {
super(context);
this.controller = controller;
this.account = account;
this.messageReference = messageReference;
}
@Override
protected void onStartLoading() {
if (message != null) {
super.deliverResult(message);
}
if (takeContentChanged() || message == null) {
forceLoad();
}
}
@Override
public void deliverResult(LocalMessage message) {
this.message = message;
super.deliverResult(message);
}
@Override
public LocalMessage loadInBackground() {
try {
return loadMessageFromDatabase();
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Error while loading message from database", e);
return null;
}
}
private LocalMessage loadMessageFromDatabase() throws MessagingException {
return controller.loadMessage(account, messageReference.folderName, messageReference.uid);
}
}

View File

@ -0,0 +1,369 @@
package com.fsck.k9.ui.messageview;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.cache.TemporaryAttachmentStore;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.helper.FileHelper;
import com.fsck.k9.helper.MediaScannerNotifier;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.AttachmentViewInfo;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalPart;
import org.apache.commons.io.IOUtils;
public class AttachmentController {
private final Context context;
private final MessagingController controller;
private final MessageViewFragment messageViewFragment;
private final AttachmentViewInfo attachment;
AttachmentController(MessagingController controller, MessageViewFragment messageViewFragment,
AttachmentViewInfo attachment) {
this.context = messageViewFragment.getContext();
this.controller = controller;
this.messageViewFragment = messageViewFragment;
this.attachment = attachment;
}
public void viewAttachment() {
if (needsDownloading()) {
downloadAndViewAttachment((LocalPart) attachment.part);
} else {
viewLocalAttachment();
}
}
public void saveAttachment() {
saveAttachmentTo(K9.getAttachmentDefaultPath());
}
public void saveAttachmentTo(String directory) {
saveAttachmentTo(new File(directory));
}
private boolean needsDownloading() {
return isPartMissing() && isLocalPart();
}
private boolean isPartMissing() {
return attachment.part.getBody() == null;
}
private boolean isLocalPart() {
return attachment.part instanceof LocalPart;
}
private void downloadAndViewAttachment(LocalPart localPart) {
downloadAttachment(localPart, new Runnable() {
@Override
public void run() {
viewLocalAttachment();
}
});
}
private void downloadAndSaveAttachmentTo(LocalPart localPart, final File directory) {
downloadAttachment(localPart, new Runnable() {
@Override
public void run() {
messageViewFragment.refreshAttachmentThumbnail(attachment);
saveAttachmentTo(directory);
}
});
}
private void downloadAttachment(LocalPart localPart, final Runnable attachmentDownloadedCallback) {
String accountUuid = localPart.getAccountUuid();
Account account = Preferences.getPreferences(context).getAccount(accountUuid);
LocalMessage message = localPart.getMessage();
messageViewFragment.showAttachmentLoadingDialog();
controller.loadAttachment(account, message, attachment.part, new MessagingListener() {
@Override
public void loadAttachmentFinished(Account account, Message message, Part part) {
messageViewFragment.hideAttachmentLoadingDialogOnMainThread();
messageViewFragment.runOnMainThread(attachmentDownloadedCallback);
}
@Override
public void loadAttachmentFailed(Account account, Message message, Part part, String reason) {
messageViewFragment.hideAttachmentLoadingDialogOnMainThread();
}
});
}
private void viewLocalAttachment() {
new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void saveAttachmentTo(File directory) {
boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
if (!isExternalStorageMounted) {
String message = context.getString(R.string.message_view_status_attachment_not_saved);
displayMessageToUser(message);
return;
}
if (needsDownloading()) {
downloadAndSaveAttachmentTo((LocalPart) attachment.part, directory);
} else {
saveLocalAttachmentTo(directory);
}
}
private void saveLocalAttachmentTo(File directory) {
new SaveAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, directory);
}
private File saveAttachmentWithUniqueFileName(File directory) throws IOException {
String filename = FileHelper.sanitizeFilename(attachment.displayName);
File file = FileHelper.createUniqueFile(directory, filename);
writeAttachmentToStorage(file);
return file;
}
private void writeAttachmentToStorage(File file) throws IOException {
InputStream in = context.getContentResolver().openInputStream(attachment.uri);
try {
OutputStream out = new FileOutputStream(file);
try {
IOUtils.copy(in, out);
out.flush();
} finally {
out.close();
}
} finally {
in.close();
}
}
private Intent getBestViewIntentAndSaveFileIfNecessary() {
String displayName = attachment.displayName;
String inferredMimeType = MimeUtility.getMimeTypeByExtension(displayName);
IntentAndResolvedActivitiesCount resolvedIntentInfo;
String mimeType = attachment.mimeType;
if (MimeUtility.isDefaultMimeType(mimeType)) {
resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType);
} else {
resolvedIntentInfo = getBestViewIntentForMimeType(mimeType);
if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(mimeType)) {
resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType);
}
}
if (!resolvedIntentInfo.hasResolvedActivities()) {
resolvedIntentInfo = getBestViewIntentForMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE);
}
Intent viewIntent;
if (resolvedIntentInfo.hasResolvedActivities() && resolvedIntentInfo.containsFileUri()) {
try {
File tempFile = TemporaryAttachmentStore.getFileForWriting(context, displayName);
writeAttachmentToStorage(tempFile);
viewIntent = createViewIntentForFileUri(resolvedIntentInfo.getMimeType(), Uri.fromFile(tempFile));
} catch (IOException e) {
if (K9.DEBUG) {
Log.e(K9.LOG_TAG, "Error while saving attachment to use file:// URI with ACTION_VIEW Intent", e);
}
viewIntent = createViewIntentForAttachmentProviderUri(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE);
}
} else {
viewIntent = resolvedIntentInfo.getIntent();
}
return viewIntent;
}
private IntentAndResolvedActivitiesCount getBestViewIntentForMimeType(String mimeType) {
Intent contentUriIntent = createViewIntentForAttachmentProviderUri(mimeType);
int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent);
if (contentUriActivitiesCount > 0) {
return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
}
File tempFile = TemporaryAttachmentStore.getFile(context, attachment.displayName);
Uri tempFileUri = Uri.fromFile(tempFile);
Intent fileUriIntent = createViewIntentForFileUri(mimeType, tempFileUri);
int fileUriActivitiesCount = getResolvedIntentActivitiesCount(fileUriIntent);
if (fileUriActivitiesCount > 0) {
return new IntentAndResolvedActivitiesCount(fileUriIntent, fileUriActivitiesCount);
}
return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
}
private Intent createViewIntentForAttachmentProviderUri(String mimeType) {
Uri uri = getAttachmentUriForMimeType(attachment, mimeType);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
addUiIntentFlags(intent);
return intent;
}
private Uri getAttachmentUriForMimeType(AttachmentViewInfo attachment, String mimeType) {
if (attachment.mimeType.equals(mimeType)) {
return attachment.uri;
}
return attachment.uri.buildUpon()
.appendPath(mimeType)
.build();
}
private Intent createViewIntentForFileUri(String mimeType, Uri uri) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
addUiIntentFlags(intent);
return intent;
}
private void addUiIntentFlags(Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
}
private int getResolvedIntentActivitiesCount(Intent intent) {
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> resolveInfos =
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
return resolveInfos.size();
}
private void displayAttachmentSavedMessage(final String filename) {
String message = context.getString(R.string.message_view_status_attachment_saved, filename);
displayMessageToUser(message);
}
private void displayAttachmentNotSavedMessage() {
String message = context.getString(R.string.message_view_status_attachment_not_saved);
displayMessageToUser(message);
}
private void displayMessageToUser(String message) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
private static class IntentAndResolvedActivitiesCount {
private Intent intent;
private int activitiesCount;
IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) {
this.intent = intent;
this.activitiesCount = activitiesCount;
}
public Intent getIntent() {
return intent;
}
public boolean hasResolvedActivities() {
return activitiesCount > 0;
}
public String getMimeType() {
return intent.getType();
}
public boolean containsFileUri() {
return "file".equals(intent.getData().getScheme());
}
}
private class ViewAttachmentAsyncTask extends AsyncTask<Void, Void, Intent> {
@Override
protected void onPreExecute() {
messageViewFragment.disableAttachmentButtons(attachment);
}
@Override
protected Intent doInBackground(Void... params) {
return getBestViewIntentAndSaveFileIfNecessary();
}
@Override
protected void onPostExecute(Intent intent) {
viewAttachment(intent);
messageViewFragment.enableAttachmentButtons(attachment);
}
private void viewAttachment(Intent intent) {
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(K9.LOG_TAG, "Could not display attachment of type " + attachment.mimeType, e);
String message = context.getString(R.string.message_view_no_viewer, attachment.mimeType);
displayMessageToUser(message);
}
}
}
private class SaveAttachmentAsyncTask extends AsyncTask<File, Void, File> {
@Override
protected void onPreExecute() {
messageViewFragment.disableAttachmentButtons(attachment);
}
@Override
protected File doInBackground(File... params) {
try {
File directory = params[0];
return saveAttachmentWithUniqueFileName(directory);
} catch (IOException e) {
if (K9.DEBUG) {
Log.e(K9.LOG_TAG, "Error saving attachment", e);
}
return null;
}
}
@Override
protected void onPostExecute(File file) {
messageViewFragment.enableAttachmentButtons(attachment);
if (file != null) {
displayAttachmentSavedMessage(file.toString());
MediaScannerNotifier.notify(context, file);
} else {
displayAttachmentNotSavedMessage();
}
}
}
}

View File

@ -0,0 +1,141 @@
package com.fsck.k9.ui.messageview;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.AttachmentViewInfo;
public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener {
private AttachmentViewInfo attachment;
private AttachmentViewCallback callback;
private Button viewButton;
private Button downloadButton;
public AttachmentView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public AttachmentView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AttachmentView(Context context) {
super(context);
}
public AttachmentViewInfo getAttachment() {
return attachment;
}
public void enableButtons() {
viewButton.setEnabled(true);
downloadButton.setEnabled(true);
}
public void disableButtons() {
viewButton.setEnabled(false);
downloadButton.setEnabled(false);
}
public void setAttachment(AttachmentViewInfo attachment) throws MessagingException {
this.attachment = attachment;
displayAttachmentInformation();
}
private void displayAttachmentInformation() {
viewButton = (Button) findViewById(R.id.view);
downloadButton = (Button) findViewById(R.id.download);
if (attachment.size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
viewButton.setVisibility(View.GONE);
downloadButton.setVisibility(View.GONE);
}
viewButton.setOnClickListener(this);
downloadButton.setOnClickListener(this);
downloadButton.setOnLongClickListener(this);
TextView attachmentName = (TextView) findViewById(R.id.attachment_name);
attachmentName.setText(attachment.displayName);
setAttachmentSize(attachment.size);
refreshThumbnail();
}
private void setAttachmentSize(long size) {
TextView attachmentSize = (TextView) findViewById(R.id.attachment_info);
if (size == AttachmentViewInfo.UNKNOWN_SIZE) {
attachmentSize.setText("");
} else {
String text = SizeFormatter.formatSize(getContext(), size);
attachmentSize.setText(text);
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.view: {
onViewButtonClick();
break;
}
case R.id.download: {
onSaveButtonClick();
break;
}
}
}
@Override
public boolean onLongClick(View view) {
if (view.getId() == R.id.download) {
onSaveButtonLongClick();
return true;
}
return false;
}
private void onViewButtonClick() {
callback.onViewAttachment(attachment);
}
private void onSaveButtonClick() {
callback.onSaveAttachment(attachment);
}
private void onSaveButtonLongClick() {
callback.onSaveAttachmentToUserProvidedDirectory(attachment);
}
public void setCallback(AttachmentViewCallback callback) {
this.callback = callback;
}
public void refreshThumbnail() {
ImageView thumbnailView = (ImageView) findViewById(R.id.attachment_icon);
Glide.with(getContext())
.load(attachment.uri)
.placeholder(R.drawable.attached_image_placeholder)
.centerCrop()
.into(thumbnailView);
}
}

View File

@ -0,0 +1,11 @@
package com.fsck.k9.ui.messageview;
import com.fsck.k9.mailstore.AttachmentViewInfo;
interface AttachmentViewCallback {
void onViewAttachment(AttachmentViewInfo attachment);
void onSaveAttachment(AttachmentViewInfo attachment);
void onSaveAttachmentToUserProvidedDirectory(AttachmentViewInfo attachment);
}

View File

@ -0,0 +1,193 @@
package com.fsck.k9.ui.messageview;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.helper.FileHelper;
import com.fsck.k9.helper.UrlEncodingHelper;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns;
import org.apache.commons.io.IOUtils;
@Deprecated
class DownloadImageTask extends AsyncTask<String, Void, String> {
private static final String[] ATTACHMENT_PROJECTION = new String[] {
AttachmentProviderColumns._ID,
AttachmentProviderColumns.DISPLAY_NAME
};
private static final int DISPLAY_NAME_INDEX = 1;
private static final String DEFAULT_FILE_NAME = "saved_image";
private final Context context;
public DownloadImageTask(Context context) {
this.context = context.getApplicationContext();
}
@Override
protected String doInBackground(String... params) {
String url = params[0];
try {
boolean isExternalImage = url.startsWith("http");
String fileName;
if (isExternalImage) {
fileName = downloadAndStoreImage(url);
} else {
fileName = fetchAndStoreImage(url);
}
return fileName;
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Error while downloading image", e);
return null;
}
}
@Override
protected void onPostExecute(String fileName) {
boolean errorSavingFile = (fileName == null);
String text;
if (errorSavingFile) {
text = context.getString(R.string.image_saving_failed);
} else {
text = context.getString(R.string.image_saved_as, fileName);
}
Toast.makeText(context, text, Toast.LENGTH_LONG).show();
}
private String downloadAndStoreImage(String urlString) throws IOException {
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();
try {
String fileName = getFileNameFromUrl(url);
String mimeType = getMimeType(conn, fileName);
String fileNameWithExtension = getFileNameWithExtension(fileName, mimeType);
return writeFileToStorage(fileNameWithExtension, in);
} finally {
in.close();
}
}
private String getFileNameFromUrl(URL url) {
String fileName;
String path = url.getPath();
int start = path.lastIndexOf("/");
if (start != -1 && start + 1 < path.length()) {
fileName = UrlEncodingHelper.decodeUtf8(path.substring(start + 1));
} else {
fileName = DEFAULT_FILE_NAME;
}
return fileName;
}
private String getMimeType(URLConnection conn, String fileName) {
String mimeType = null;
if (fileName.indexOf('.') == -1) {
mimeType = conn.getContentType();
}
return mimeType;
}
private String fetchAndStoreImage(String urlString) throws IOException {
ContentResolver contentResolver = context.getContentResolver();
Uri uri = Uri.parse(urlString);
String fileName = getFileNameFromContentProvider(contentResolver, uri);
String mimeType = getMimeType(contentResolver, uri, fileName);
InputStream in = contentResolver.openInputStream(uri);
try {
String fileNameWithExtension = getFileNameWithExtension(fileName, mimeType);
return writeFileToStorage(fileNameWithExtension, in);
} finally {
in.close();
}
}
private String getMimeType(ContentResolver contentResolver, Uri uri, String fileName) {
String mimeType = null;
if (fileName.indexOf('.') == -1) {
mimeType = contentResolver.getType(uri);
}
return mimeType;
}
private String getFileNameFromContentProvider(ContentResolver contentResolver, Uri uri) {
String displayName = DEFAULT_FILE_NAME;
Cursor cursor = contentResolver.query(uri, ATTACHMENT_PROJECTION, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToNext() && !cursor.isNull(DISPLAY_NAME_INDEX)) {
displayName = cursor.getString(DISPLAY_NAME_INDEX);
}
} finally {
cursor.close();
}
}
return displayName;
}
private String getFileNameWithExtension(String fileName, String mimeType) {
if (fileName.indexOf('.') != -1) {
return fileName;
}
// Use JPEG as fallback
String extension = "jpeg";
if (mimeType != null) {
String extensionFromMimeType = MimeUtility.getExtensionByMimeType(mimeType);
if (extensionFromMimeType != null) {
extension = extensionFromMimeType;
}
}
return fileName + "." + extension;
}
private String writeFileToStorage(String fileName, InputStream in) throws IOException {
String sanitized = FileHelper.sanitizeFilename(fileName);
File directory = new File(K9.getAttachmentDefaultPath());
File file = FileHelper.createUniqueFile(directory, sanitized);
FileOutputStream out = new FileOutputStream(file);
try {
IOUtils.copy(in, out);
out.flush();
} finally {
out.close();
}
return file.getName();
}
}

View File

@ -1,25 +1,15 @@
package com.fsck.k9.view;
package com.fsck.k9.ui.messageview;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import android.app.Activity;
import android.app.Fragment;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
@ -30,41 +20,32 @@ import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.ViewStub;
import android.webkit.WebView;
import android.webkit.WebView.HitTestResult;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.MessageViewFragment;
import com.fsck.k9.helper.ClipboardManager;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.helper.FileHelper;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.UrlEncodingHelper;
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.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.LocalAttachmentBodyPart;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.provider.AttachmentProvider.AttachmentProviderColumns;
import com.fsck.k9.mailstore.AttachmentViewInfo;
import com.fsck.k9.mailstore.MessageViewInfo.MessageViewContainer;
import org.apache.commons.io.IOUtils;
import com.fsck.k9.mailstore.OpenPgpResultAnnotation;
import com.fsck.k9.mailstore.OpenPgpResultAnnotation.CryptoError;
import com.fsck.k9.view.K9WebViewClient;
import com.fsck.k9.view.MessageHeader.OnLayoutChangedListener;
import com.fsck.k9.view.MessageWebView;
public class SingleMessageView extends LinearLayout implements OnClickListener,
MessageHeader.OnLayoutChangedListener, OnCreateContextMenuListener {
public class MessageContainerView extends LinearLayout implements OnClickListener,
OnLayoutChangedListener, OnCreateContextMenuListener {
private static final int MENU_ITEM_LINK_VIEW = Menu.FIRST;
private static final int MENU_ITEM_LINK_SHARE = Menu.FIRST + 1;
private static final int MENU_ITEM_LINK_COPY = Menu.FIRST + 2;
@ -81,43 +62,29 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
private static final int MENU_ITEM_EMAIL_SAVE = Menu.FIRST + 1;
private static final int MENU_ITEM_EMAIL_COPY = Menu.FIRST + 2;
private static final String[] ATTACHMENT_PROJECTION = new String[] {
AttachmentProviderColumns._ID,
AttachmentProviderColumns.DISPLAY_NAME
};
private static final int DISPLAY_NAME_INDEX = 1;
private MessageOpenPgpView mOpenPgpView;
private View mSidebar;
private MessageWebView mMessageContentView;
private MessageHeader mHeaderContainer;
private LinearLayout mAttachments;
private Button mShowHiddenAttachments;
private LinearLayout mHiddenAttachments;
private View mShowPicturesAction;
private View mShowMessageAction;
private View mShowAttachmentsAction;
private boolean mShowPictures;
private boolean mHasAttachments;
private Button mDownloadRemainder;
private boolean showingPictures;
private LayoutInflater mInflater;
private Contacts mContacts;
private AttachmentView.AttachmentFileDownloadCallback attachmentCallback;
private AttachmentViewCallback attachmentCallback;
private View mAttachmentsContainer;
private SavedState mSavedState;
private ClipboardManager mClipboardManager;
private String mText;
private Map<AttachmentViewInfo, AttachmentView> attachments = new HashMap<AttachmentViewInfo, AttachmentView>();
public void initialize(Fragment fragment) {
Activity activity = fragment.getActivity();
@Override
public void onFinishInflate() {
mSidebar = findViewById(R.id.message_sidebar);
mMessageContentView = (MessageWebView) findViewById(R.id.message_content);
mMessageContentView.configure();
activity.registerForContextMenu(mMessageContentView);
mMessageContentView.setOnCreateContextMenuListener(this);
mHeaderContainer = (MessageHeader) findViewById(R.id.header_container);
mHeaderContainer.setOnLayoutChangedListener(this);
mMessageContentView.setVisibility(View.VISIBLE);
mAttachmentsContainer = findViewById(R.id.attachments_container);
mAttachments = (LinearLayout) findViewById(R.id.attachments);
@ -125,38 +92,13 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
mHiddenAttachments.setVisibility(View.GONE);
mShowHiddenAttachments = (Button) findViewById(R.id.show_hidden_attachments);
mShowHiddenAttachments.setVisibility(View.GONE);
mOpenPgpView = (MessageOpenPgpView) findViewById(R.id.layout_decrypt_openpgp);
mOpenPgpView.setFragment(fragment);
mOpenPgpView.setupChildViews();
mShowPicturesAction = findViewById(R.id.show_pictures);
mShowMessageAction = findViewById(R.id.show_message);
mShowAttachmentsAction = findViewById(R.id.show_attachments);
mShowPictures = false;
mContacts = Contacts.getInstance(activity);
mInflater = ((MessageViewFragment) fragment).getFragmentLayoutInflater();
mDownloadRemainder = (Button) findViewById(R.id.download_remainder);
mDownloadRemainder.setVisibility(View.GONE);
mAttachmentsContainer.setVisibility(View.GONE);
mMessageContentView.setVisibility(View.VISIBLE);
// the HTC version of WebView tries to force the background of the
// titlebar, which is really unfair.
TypedValue outValue = new TypedValue();
getContext().getTheme().resolveAttribute(R.attr.messageViewHeaderBackgroundColor, outValue, true);
mHeaderContainer.setBackgroundColor(outValue.data);
// also set background of the whole view (including the attachments view)
setBackgroundColor(outValue.data);
mShowHiddenAttachments.setOnClickListener(this);
mShowMessageAction.setOnClickListener(this);
mShowAttachmentsAction.setOnClickListener(this);
mShowPicturesAction.setOnClickListener(this);
mClipboardManager = ClipboardManager.getInstance(activity);
showingPictures = false;
Context context = getContext();
mInflater = LayoutInflater.from(context);
mClipboardManager = ClipboardManager.getInstance(context);
}
@Override
@ -238,7 +180,8 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
break;
}
case MENU_ITEM_IMAGE_SAVE: {
new DownloadImageTask().execute(url);
//TODO: Use download manager
new DownloadImageTask(getContext()).execute(url);
break;
}
case MENU_ITEM_IMAGE_COPY: {
@ -381,21 +324,6 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
onShowHiddenAttachments();
break;
}
case R.id.show_message: {
onShowMessage();
break;
}
case R.id.show_attachments: {
onShowAttachments();
break;
}
case R.id.show_pictures: {
// Allow network access first...
setLoadPictures(true);
// ...then re-populate the WebView with the message text
loadBodyFromText(mText);
break;
}
}
}
@ -404,230 +332,147 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
mHiddenAttachments.setVisibility(View.VISIBLE);
}
public void onShowMessage() {
showShowMessageAction(false);
showAttachments(false);
showShowAttachmentsAction(mHasAttachments);
showMessageWebView(true);
}
public void onShowAttachments() {
showMessageWebView(false);
showShowAttachmentsAction(false);
showShowMessageAction(true);
showAttachments(true);
}
public SingleMessageView(Context context, AttributeSet attrs) {
public MessageContainerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean showPictures() {
return mShowPictures;
private boolean isShowingPictures() {
return showingPictures;
}
public void setShowPictures(Boolean show) {
mShowPictures = show;
}
/**
* Enable/disable image loading of the WebView. But always hide the
* "Show pictures" button!
*
* @param enable true, if (network) images should be loaded.
* false, otherwise.
*/
public void setLoadPictures(boolean enable) {
private void setLoadPictures(boolean enable) {
mMessageContentView.blockNetworkData(!enable);
setShowPictures(enable);
showShowPicturesAction(false);
showingPictures = enable;
}
public Button downloadRemainderButton() {
return mDownloadRemainder;
public void showPictures() {
setLoadPictures(true);
loadBodyFromText(mText);
}
public void showShowPicturesAction(boolean show) {
mShowPicturesAction.setVisibility(show ? View.VISIBLE : View.GONE);
}
public void showShowMessageAction(boolean show) {
mShowMessageAction.setVisibility(show ? View.VISIBLE : View.GONE);
}
public void showShowAttachmentsAction(boolean show) {
mShowAttachmentsAction.setVisibility(show ? View.VISIBLE : View.GONE);
}
/**
* Fetch the message header view. This is not the same as the message headers; this is the View shown at the top
* of messages.
* @return MessageHeader View.
*/
public MessageHeader getMessageHeaderView() {
return mHeaderContainer;
}
public void setHeaders(final Message message, Account account) {
try {
mHeaderContainer.populate(message, account);
mHeaderContainer.setVisibility(View.VISIBLE);
} catch (Exception me) {
Log.e(K9.LOG_TAG, "setHeaders - error", me);
public void enableAttachmentButtons() {
for (AttachmentView attachmentView : attachments.values()) {
attachmentView.enableButtons();
}
}
public void setShowDownloadButton(Message message) {
if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
mDownloadRemainder.setVisibility(View.GONE);
} else {
mDownloadRemainder.setEnabled(true);
mDownloadRemainder.setVisibility(View.VISIBLE);
public void disableAttachmentButtons() {
for (AttachmentView attachmentView : attachments.values()) {
attachmentView.disableButtons();
}
}
public void setOnFlagListener(OnClickListener listener) {
mHeaderContainer.setOnFlagListener(listener);
}
public void displayMessageViewContainer(MessageViewContainer messageViewContainer,
boolean automaticallyLoadPictures, ShowPicturesController showPicturesController,
AttachmentViewCallback attachmentCallback, OpenPgpHeaderViewCallback openPgpHeaderViewCallback,
boolean displayPgpHeader) throws MessagingException {
public void showAllHeaders() {
mHeaderContainer.onShowAdditionalHeaders();
}
this.attachmentCallback = attachmentCallback;
public boolean additionalHeadersVisible() {
return mHeaderContainer.additionalHeadersVisible();
}
public void setMessage(Account account, LocalMessage message, PgpData pgpData,
MessagingController controller, MessagingListener listener) throws MessagingException {
resetView();
String text = null;
if (pgpData != null) {
text = pgpData.getDecryptedData();
if (text != null) {
text = HtmlConverter.textToHtml(text);
}
}
WebViewClient webViewClient = K9WebViewClient.newInstance(messageViewContainer.rootPart);
mMessageContentView.setWebViewClient(webViewClient);
if (text == null) {
text = message.getTextForDisplay();
}
// Save the text so we can reset the WebView when the user clicks the "Show pictures" button
mText = text;
mHasAttachments = message.hasAttachments();
if (mHasAttachments) {
renderAttachments(message, 0, message, account, controller, listener);
boolean hasAttachments = !messageViewContainer.attachments.isEmpty();
if (hasAttachments) {
renderAttachments(messageViewContainer);
}
mHiddenAttachments.setVisibility(View.GONE);
boolean lookForImages = true;
if (mSavedState != null) {
if (mSavedState.showPictures) {
if (mSavedState.showingPictures) {
setLoadPictures(true);
lookForImages = false;
}
if (mSavedState.attachmentViewVisible) {
onShowAttachments();
} else {
onShowMessage();
}
if (mSavedState.hiddenAttachmentsVisible) {
onShowHiddenAttachments();
}
mSavedState = null;
} else {
onShowMessage();
}
if (text != null && lookForImages) {
// If the message contains external pictures and the "Show pictures"
// button wasn't already pressed, see if the user's preferences has us
// showing them anyway.
if (Utility.hasExternalImages(text) && !showPictures()) {
Address[] from = message.getFrom();
if ((account.getShowPictures() == Account.ShowPictures.ALWAYS) ||
((account.getShowPictures() == Account.ShowPictures.ONLY_FROM_CONTACTS) &&
// Make sure we have at least one from address
(from != null && from.length > 0) &&
mContacts.isInContacts(from[0].getAddress()))) {
mText = getTextToDisplay(messageViewContainer);
if (mText != null && lookForImages) {
if (Utility.hasExternalImages(mText) && !isShowingPictures()) {
if (automaticallyLoadPictures) {
setLoadPictures(true);
} else {
showShowPicturesAction(true);
showPicturesController.notifyMessageContainerContainsPictures(this);
}
}
}
if (text != null) {
loadBodyFromText(text);
mOpenPgpView.updateLayout(account, pgpData.getDecryptedData(),
pgpData.getSignatureResult(), message);
if (displayPgpHeader) {
ViewStub openPgpHeaderStub = (ViewStub) findViewById(R.id.openpgp_header_stub);
OpenPgpHeaderView openPgpHeaderView = (OpenPgpHeaderView) openPgpHeaderStub.inflate();
OpenPgpResultAnnotation cryptoAnnotation = messageViewContainer.cryptoAnnotation;
openPgpHeaderView.setOpenPgpData(cryptoAnnotation);
openPgpHeaderView.setCallback(openPgpHeaderViewCallback);
mSidebar.setVisibility(View.VISIBLE);
} else {
showStatusMessage(getContext().getString(R.string.webview_empty_message));
mSidebar.setVisibility(View.GONE);
}
String text;
if (mText != null) {
text = mText;
} else {
text = wrapStatusMessage(getContext().getString(R.string.webview_empty_message));
}
loadBodyFromText(text);
}
public void showStatusMessage(String status) {
String text = "<div style=\"text-align:center; color: grey;\">" + status + "</div>";
loadBodyFromText(text);
private String getTextToDisplay(MessageViewContainer messageViewContainer) {
OpenPgpResultAnnotation cryptoAnnotation = messageViewContainer.cryptoAnnotation;
if (cryptoAnnotation == null) {
return messageViewContainer.text;
}
CryptoError errorType = cryptoAnnotation.getErrorType();
switch (errorType) {
case CRYPTO_API_RETURNED_ERROR: {
// TODO make a nice view for this
return wrapStatusMessage(cryptoAnnotation.getError().getMessage());
}
case ENCRYPTED_BUT_INCOMPLETE: {
return wrapStatusMessage(getContext().getString(R.string.crypto_download_complete_message_to_decrypt));
}
case NONE:
case SIGNED_BUT_INCOMPLETE: {
return messageViewContainer.text;
}
}
throw new IllegalStateException("Unknown error type: " + errorType);
}
public String wrapStatusMessage(String status) {
return "<div style=\"text-align:center; color: grey;\">" + status + "</div>";
}
private void loadBodyFromText(String emailText) {
mMessageContentView.setText(emailText);
}
public void showAttachments(boolean show) {
mAttachmentsContainer.setVisibility(show ? View.VISIBLE : View.GONE);
boolean showHidden = (show && mHiddenAttachments.getVisibility() == View.GONE &&
mHiddenAttachments.getChildCount() > 0);
mShowHiddenAttachments.setVisibility(showHidden ? View.VISIBLE : View.GONE);
}
public void showMessageWebView(boolean show) {
mMessageContentView.setVisibility(show ? View.VISIBLE : View.GONE);
}
public void setAttachmentsEnabled(boolean enabled) {
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
AttachmentView attachment = (AttachmentView) mAttachments.getChildAt(i);
attachment.setButtonsEnabled(enabled);
}
}
public void removeAllAttachments() {
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
mAttachments.removeView(mAttachments.getChildAt(i));
}
}
public void renderAttachments(Part part, int depth, Message message, Account account,
MessagingController controller, MessagingListener listener) throws MessagingException {
if (part.getBody() instanceof Multipart) {
Multipart mp = (Multipart) part.getBody();
for (int i = 0; i < mp.getCount(); i++) {
renderAttachments(mp.getBodyPart(i), depth + 1, message, account, controller, listener);
}
} else if (part instanceof LocalAttachmentBodyPart) {
AttachmentView view = (AttachmentView)mInflater.inflate(R.layout.message_view_attachment, null);
public void renderAttachments(MessageViewContainer messageContainer) throws MessagingException {
for (AttachmentViewInfo attachment : messageContainer.attachments) {
AttachmentView view = (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, null);
view.setCallback(attachmentCallback);
view.setAttachment(attachment);
try {
if (view.populateFromPart(part, message, account, controller, listener)) {
addAttachment(view);
} else {
addHiddenAttachment(view);
}
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Error adding attachment view", e);
attachments.put(attachment, view);
if (attachment.firstClassAttachment) {
addAttachment(view);
} else {
addHiddenAttachment(view);
}
}
}
@ -653,11 +498,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
}
public void resetView() {
mDownloadRemainder.setVisibility(View.GONE);
setLoadPictures(false);
showShowAttachmentsAction(false);
showShowMessageAction(false);
showShowPicturesAction(false);
mAttachments.removeAllViews();
mHiddenAttachments.removeAllViews();
@ -671,19 +512,6 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
loadBodyFromText("");
}
public void resetHeaderView() {
mHeaderContainer.setVisibility(View.GONE);
}
public AttachmentView.AttachmentFileDownloadCallback getAttachmentCallback() {
return attachmentCallback;
}
public void setAttachmentCallback(
AttachmentView.AttachmentFileDownloadCallback attachmentCallback) {
this.attachmentCallback = attachmentCallback;
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
@ -694,7 +522,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
mAttachmentsContainer.getVisibility() == View.VISIBLE);
savedState.hiddenAttachmentsVisible = (mHiddenAttachments != null &&
mHiddenAttachments.getVisibility() == View.VISIBLE);
savedState.showPictures = mShowPictures;
savedState.showingPictures = showingPictures;
return savedState;
}
@ -719,10 +547,26 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
}
}
public void enableAttachmentButtons(AttachmentViewInfo attachment) {
getAttachmentView(attachment).enableButtons();
}
public void disableAttachmentButtons(AttachmentViewInfo attachment) {
getAttachmentView(attachment).disableButtons();
}
public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) {
getAttachmentView(attachment).refreshThumbnail();
}
private AttachmentView getAttachmentView(AttachmentViewInfo attachment) {
return attachments.get(attachment);
}
static class SavedState extends BaseSavedState {
boolean attachmentViewVisible;
boolean hiddenAttachmentsVisible;
boolean showPictures;
boolean showingPictures;
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@ -746,7 +590,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
super(in);
this.attachmentViewVisible = (in.readInt() != 0);
this.hiddenAttachmentsVisible = (in.readInt() != 0);
this.showPictures = (in.readInt() != 0);
this.showingPictures = (in.readInt() != 0);
}
@Override
@ -754,120 +598,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
super.writeToParcel(out, flags);
out.writeInt((this.attachmentViewVisible) ? 1 : 0);
out.writeInt((this.hiddenAttachmentsVisible) ? 1 : 0);
out.writeInt((this.showPictures) ? 1 : 0);
}
}
class DownloadImageTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... params) {
String urlString = params[0];
try {
boolean externalImage = urlString.startsWith("http");
String filename = null;
String mimeType = null;
InputStream in = null;
try {
if (externalImage) {
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
in = conn.getInputStream();
String path = url.getPath();
// Try to get the filename from the URL
int start = path.lastIndexOf("/");
if (start != -1 && start + 1 < path.length()) {
filename = UrlEncodingHelper.decodeUtf8(path.substring(start + 1));
} else {
// Use a dummy filename if necessary
filename = "saved_image";
}
// Get the MIME type if we couldn't find a file extension
if (filename.indexOf('.') == -1) {
mimeType = conn.getContentType();
}
} else {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri.parse(urlString);
// Get the filename from AttachmentProvider
Cursor cursor = contentResolver.query(uri, ATTACHMENT_PROJECTION, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToNext()) {
filename = cursor.getString(DISPLAY_NAME_INDEX);
}
} finally {
cursor.close();
}
}
// Use a dummy filename if necessary
if (filename == null) {
filename = "saved_image";
}
// Get the MIME type if we couldn't find a file extension
if (filename.indexOf('.') == -1) {
mimeType = contentResolver.getType(uri);
}
in = contentResolver.openInputStream(uri);
}
// Do we still need an extension?
if (filename.indexOf('.') == -1) {
// Use JPEG as fallback
String extension = "jpeg";
if (mimeType != null) {
// Try to find an extension for the given MIME type
String ext = MimeUtility.getExtensionByMimeType(mimeType);
if (ext != null) {
extension = ext;
}
}
filename += "." + extension;
}
String sanitized = FileHelper.sanitizeFilename(filename);
File directory = new File(K9.getAttachmentDefaultPath());
File file = FileHelper.createUniqueFile(directory, sanitized);
FileOutputStream out = new FileOutputStream(file);
try {
IOUtils.copy(in, out);
out.flush();
} finally {
out.close();
}
return file.getName();
} finally {
if (in != null) {
in.close();
}
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onPostExecute(String filename) {
String text;
if (filename == null) {
text = getContext().getString(R.string.image_saving_failed);
} else {
text = getContext().getString(R.string.image_saved_as, filename);
}
Toast.makeText(getContext(), text, Toast.LENGTH_LONG).show();
out.writeInt((this.showingPictures) ? 1 : 0);
}
}
}

View File

@ -0,0 +1,206 @@
package com.fsck.k9.ui.messageview;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import com.fsck.k9.Account;
import com.fsck.k9.Account.ShowPictures;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.mailstore.MessageViewInfo.MessageViewContainer;
import com.fsck.k9.view.MessageHeader;
public class MessageTopView extends LinearLayout implements ShowPicturesController {
private MessageHeader mHeaderContainer;
private LayoutInflater mInflater;
private LinearLayout containerViews;
private Button mDownloadRemainder;
private AttachmentViewCallback attachmentCallback;
private OpenPgpHeaderViewCallback openPgpHeaderViewCallback;
private Button showPicturesButton;
private List<MessageContainerView> messageContainerViewsWithPictures = new ArrayList<MessageContainerView>();
public MessageTopView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onFinishInflate() {
mHeaderContainer = (MessageHeader) findViewById(R.id.header_container);
// mHeaderContainer.setOnLayoutChangedListener(this);
mInflater = LayoutInflater.from(getContext());
mDownloadRemainder = (Button) findViewById(R.id.download_remainder);
mDownloadRemainder.setVisibility(View.GONE);
showPicturesButton = (Button) findViewById(R.id.show_pictures);
setShowPicturesButtonListener();
containerViews = (LinearLayout) findViewById(R.id.message_containers);
}
private void setShowPicturesButtonListener() {
showPicturesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showPicturesInAllContainerViews();
}
});
}
private void showPicturesInAllContainerViews() {
for (MessageContainerView containerView : messageContainerViewsWithPictures) {
containerView.showPictures();
}
hideShowPicturesButton();
}
public void resetView() {
mDownloadRemainder.setVisibility(View.GONE);
containerViews.removeAllViews();
}
public void setMessage(Account account, MessageViewInfo messageViewInfo)
throws MessagingException {
resetView();
ShowPictures showPicturesSetting = account.getShowPictures();
boolean automaticallyLoadPictures =
shouldAutomaticallyLoadPictures(showPicturesSetting, messageViewInfo.message);
for (MessageViewContainer container : messageViewInfo.containers) {
MessageContainerView view = (MessageContainerView) mInflater.inflate(R.layout.message_container, null);
boolean displayPgpHeader = account.isOpenPgpProviderConfigured();
view.displayMessageViewContainer(container, automaticallyLoadPictures, this, attachmentCallback,
openPgpHeaderViewCallback, displayPgpHeader);
containerViews.addView(view);
}
}
/**
* Fetch the message header view. This is not the same as the message headers; this is the View shown at the top
* of messages.
* @return MessageHeader View.
*/
public MessageHeader getMessageHeaderView() {
return mHeaderContainer;
}
public void setHeaders(final Message message, Account account) {
try {
mHeaderContainer.populate(message, account);
mHeaderContainer.setVisibility(View.VISIBLE);
} catch (Exception me) {
Log.e(K9.LOG_TAG, "setHeaders - error", me);
}
}
public void setOnToggleFlagClickListener(OnClickListener listener) {
mHeaderContainer.setOnFlagListener(listener);
}
public void showAllHeaders() {
mHeaderContainer.onShowAdditionalHeaders();
}
public boolean additionalHeadersVisible() {
return mHeaderContainer.additionalHeadersVisible();
}
public void resetHeaderView() {
mHeaderContainer.setVisibility(View.GONE);
}
public void setOnDownloadButtonClickListener(OnClickListener listener) {
mDownloadRemainder.setOnClickListener(listener);
}
public void setAttachmentCallback(AttachmentViewCallback callback) {
attachmentCallback = callback;
}
public void setOpenPgpHeaderViewCallback(OpenPgpHeaderViewCallback callback) {
openPgpHeaderViewCallback = callback;
}
public void enableDownloadButton() {
mDownloadRemainder.setEnabled(true);
}
public void disableDownloadButton() {
mDownloadRemainder.setEnabled(false);
}
public void setShowDownloadButton(Message message) {
if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
mDownloadRemainder.setVisibility(View.GONE);
} else {
mDownloadRemainder.setEnabled(true);
mDownloadRemainder.setVisibility(View.VISIBLE);
}
}
private void showShowPicturesButton() {
showPicturesButton.setVisibility(View.VISIBLE);
}
private void hideShowPicturesButton() {
showPicturesButton.setVisibility(View.GONE);
}
@Override
public void notifyMessageContainerContainsPictures(MessageContainerView messageContainerView) {
messageContainerViewsWithPictures.add(messageContainerView);
showShowPicturesButton();
}
private boolean shouldAutomaticallyLoadPictures(ShowPictures showPicturesSetting, Message message) {
return showPicturesSetting == ShowPictures.ALWAYS || shouldShowPicturesFromSender(showPicturesSetting, message);
}
private boolean shouldShowPicturesFromSender(ShowPictures showPicturesSetting, Message message) {
if (showPicturesSetting != ShowPictures.ONLY_FROM_CONTACTS) {
return false;
}
String senderEmailAddress = getSenderEmailAddress(message);
if (senderEmailAddress == null) {
return false;
}
Contacts contacts = Contacts.getInstance(getContext());
return contacts.isInContacts(senderEmailAddress);
}
private String getSenderEmailAddress(Message message) {
Address[] from = message.getFrom();
if (from == null || from.length == 0) {
return null;
}
return from[0].getAddress();
}
}

View File

@ -1,18 +1,23 @@
package com.fsck.k9.fragment;
package com.fsck.k9.ui.messageview;
import java.io.File;
import java.util.Collections;
import java.util.Locale;
import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.Loader;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
@ -31,24 +36,25 @@ import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.fragment.ProgressDialogFragment;
import com.fsck.k9.helper.FileBrowserHelper;
import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mailstore.AttachmentViewInfo;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.view.AttachmentView;
import com.fsck.k9.view.AttachmentView.AttachmentFileDownloadCallback;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.ui.crypto.MessageCryptoCallback;
import com.fsck.k9.ui.crypto.MessageCryptoHelper;
import com.fsck.k9.ui.message.DecodeMessageLoader;
import com.fsck.k9.ui.message.LocalMessageLoader;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
import com.fsck.k9.view.MessageHeader;
import com.fsck.k9.view.SingleMessageView;
import org.openintents.openpgp.OpenPgpSignatureResult;
public class MessageViewFragment extends Fragment implements OnClickListener,
ConfirmationDialogFragmentListener {
public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener,
AttachmentViewCallback, OpenPgpHeaderViewCallback, MessageCryptoCallback {
private static final String ARG_REFERENCE = "reference";
@ -59,6 +65,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;
private static final int ACTIVITY_CHOOSE_DIRECTORY = 3;
private static final int LOCAL_MESSAGE_LOADER_ID = 1;
private static final int DECODE_MESSAGE_LOADER_ID = 2;
public static MessageViewFragment newInstance(MessageReference reference) {
MessageViewFragment fragment = new MessageViewFragment();
@ -70,22 +78,16 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
return fragment;
}
private SingleMessageView mMessageView;
private MessageTopView mMessageView;
private PgpData mPgpData;
private Account mAccount;
private MessageReference mMessageReference;
private LocalMessage mMessage;
private MessageCryptoAnnotations messageAnnotations;
private MessagingController mController;
private Listener mListener = new Listener();
private MessageViewHandler mHandler = new MessageViewHandler();
private LayoutInflater mLayoutInflater;
/** this variable is used to save the calling AttachmentView
* until the onActivityResult is called.
* => with this reference we can identity the caller
*/
private AttachmentView attachmentTmpStore;
private Handler handler = new Handler();
private DownloadMessageListener downloadMessageListener = new DownloadMessageListener();
private MessageCryptoHelper messageCryptoHelper;
/**
* Used to temporarily store the destination folder for refile operations if a confirmation
@ -104,66 +106,10 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
private Context mContext;
class MessageViewHandler extends Handler {
public void progress(final boolean progress) {
post(new Runnable() {
@Override
public void run() {
setProgress(progress);
}
});
}
public void addAttachment(final View attachmentView) {
post(new Runnable() {
@Override
public void run() {
mMessageView.addAttachment(attachmentView);
}
});
}
/* A helper for a set of "show a toast" methods */
private void showToast(final String message, final int toastLength) {
post(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), message, toastLength).show();
}
});
}
public void networkError() {
// FIXME: This is a hack. Fix the Handler madness!
Context context = getActivity();
if (context == null) {
return;
}
showToast(context.getString(R.string.status_network_error), Toast.LENGTH_LONG);
}
public void invalidIdError() {
Context context = getActivity();
if (context == null) {
return;
}
showToast(context.getString(R.string.status_invalid_id_error), Toast.LENGTH_LONG);
}
public void fetchingAttachment() {
Context context = getActivity();
if (context == null) {
return;
}
showToast(context.getString(R.string.message_view_fetching_attachment_toast), Toast.LENGTH_SHORT);
}
}
private LoaderCallbacks<LocalMessage> localMessageLoaderCallback = new LocalMessageLoaderCallback();
private LoaderCallbacks<MessageViewInfo> decodeMessageLoaderCallback = new DecodeMessageLoaderCallback();
private MessageViewInfo messageViewInfo;
private AttachmentViewInfo currentAttachmentViewInfo;
@Override
public void onAttach(Activity activity) {
@ -195,42 +141,26 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
Bundle savedInstanceState) {
Context context = new ContextThemeWrapper(inflater.getContext(),
K9.getK9ThemeResourceId(K9.getK9MessageViewTheme()));
mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = mLayoutInflater.inflate(R.layout.message, container, false);
LayoutInflater layoutInflater = LayoutInflater.from(context);
View view = layoutInflater.inflate(R.layout.message, container, false);
mMessageView = (MessageTopView) view.findViewById(R.id.message_view);
mMessageView.setAttachmentCallback(this);
mMessageView.setOpenPgpHeaderViewCallback(this);
mMessageView = (SingleMessageView) view.findViewById(R.id.message_view);
//set a callback for the attachment view. With this callback the attachmentview
//request the start of a filebrowser activity.
mMessageView.setAttachmentCallback(new AttachmentFileDownloadCallback() {
mMessageView.setOnToggleFlagClickListener(new OnClickListener() {
@Override
public void pickDirectoryToSaveAttachmentTo(final AttachmentView caller) {
FileBrowserHelper.getInstance()
.showFileBrowserActivity(MessageViewFragment.this,
null,
ACTIVITY_CHOOSE_DIRECTORY,
callback);
attachmentTmpStore = caller;
public void onClick(View v) {
onToggleFlagged();
}
FileBrowserFailOverCallback callback = new FileBrowserFailOverCallback() {
@Override
public void onPathEntered(String path) {
attachmentTmpStore.writeFile(new File(path));
}
@Override
public void onCancel() {
// canceled, do nothing
}
};
});
mMessageView.initialize(this);
mMessageView.downloadRemainderButton().setOnClickListener(this);
mMessageView.setOnDownloadButtonClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onDownloadRemainder();
}
});
mFragmentListener.messageHeaderViewAvailable(mMessageView.getMessageHeaderView());
@ -247,7 +177,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
messageReference = (MessageReference) savedInstanceState.get(STATE_MESSAGE_REFERENCE);
} else {
Bundle args = getArguments();
messageReference = (MessageReference) args.getParcelable(ARG_REFERENCE);
messageReference = args.getParcelable(ARG_REFERENCE);
}
displayMessage(messageReference, (mPgpData == null));
@ -260,10 +190,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
outState.putSerializable(STATE_PGP_DATA, mPgpData);
}
public void displayMessage(MessageReference ref) {
displayMessage(ref, true);
}
private void displayMessage(MessageReference ref, boolean resetPgpData) {
mMessageReference = ref;
if (K9.DEBUG) {
@ -272,6 +198,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
Context appContext = getActivity().getApplicationContext();
mAccount = Preferences.getPreferences(appContext).getAccount(mMessageReference.accountUuid);
messageCryptoHelper = new MessageCryptoHelper(getActivity(), mAccount, this);
if (resetPgpData) {
// start with fresh, empty PGP data
@ -282,11 +209,90 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
mMessageView.resetView();
mMessageView.resetHeaderView();
mController.loadMessageForView(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener);
startLoadingMessageFromDatabase();
mFragmentListener.updateMenu();
}
public void handleCryptoResult(int requestCode, int resultCode, Intent data) {
if (messageCryptoHelper != null) {
messageCryptoHelper.handleCryptoResult(requestCode, resultCode, data);
}
}
private void startLoadingMessageFromDatabase() {
getLoaderManager().initLoader(LOCAL_MESSAGE_LOADER_ID, null, localMessageLoaderCallback);
}
private void onLoadMessageFromDatabaseFinished(LocalMessage message) {
displayMessageHeader(message);
if (message.isBodyMissing()) {
startDownloadingMessageBody(message);
} else {
messageCryptoHelper.decryptOrVerifyMessagePartsIfNecessary(message);
}
}
private void onLoadMessageFromDatabaseFailed() {
// mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error));
}
private void startDownloadingMessageBody(LocalMessage message) {
throw new RuntimeException("Not implemented yet");
}
private void onMessageDownloadFinished(LocalMessage message) {
mMessage = message;
LoaderManager loaderManager = getLoaderManager();
loaderManager.destroyLoader(LOCAL_MESSAGE_LOADER_ID);
loaderManager.destroyLoader(DECODE_MESSAGE_LOADER_ID);
onLoadMessageFromDatabaseFinished(mMessage);
}
private void onDownloadMessageFailed(Throwable t) {
mMessageView.enableDownloadButton();
String errorMessage;
if (t instanceof IllegalArgumentException) {
errorMessage = mContext.getString(R.string.status_invalid_id_error);
} else {
errorMessage = mContext.getString(R.string.status_network_error);
}
Toast.makeText(mContext, errorMessage, Toast.LENGTH_LONG).show();
}
@Override
public void onCryptoOperationsFinished(MessageCryptoAnnotations annotations) {
startExtractingTextAndAttachments(annotations);
}
private void startExtractingTextAndAttachments(MessageCryptoAnnotations annotations) {
this.messageAnnotations = annotations;
getLoaderManager().initLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback);
}
private void onDecodeMessageFinished(MessageViewInfo messageContainer) {
this.messageViewInfo = messageContainer;
showMessage(messageContainer);
}
private void showMessage(MessageViewInfo messageContainer) {
try {
mMessageView.setMessage(mAccount, messageContainer);
mMessageView.setShowDownloadButton(mMessage);
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Error while trying to display message", e);
}
}
private void displayMessageHeader(LocalMessage message) {
mMessageView.setHeaders(message, mAccount);
displayMessageSubject(getSubjectForMessage(message));
mFragmentListener.updateMenu();
}
/**
* Called from UI thread when user select Delete
*/
@ -411,7 +417,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
}
public void onSelectText() {
mMessageView.beginSelectingText();
// FIXME
// mMessageView.beginSelectingText();
}
private void startRefileActivity(int activity) {
@ -423,7 +430,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
startActivityForResult(intent, activity);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
@ -432,13 +438,13 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
switch (requestCode) {
case ACTIVITY_CHOOSE_DIRECTORY: {
if (resultCode == Activity.RESULT_OK && data != null) {
if (data != null) {
// obtain the filename
Uri fileUri = data.getData();
if (fileUri != null) {
String filePath = fileUri.getPath();
if (filePath != null) {
attachmentTmpStore.writeFile(new File(filePath));
getAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(filePath);
}
}
}
@ -492,15 +498,10 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
if (mMessage.isSet(Flag.X_DOWNLOADED_FULL)) {
return;
}
mMessageView.downloadRemainderButton().setEnabled(false);
mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid, mListener);
}
mMessageView.disableDownloadButton();
@Override
public void onClick(View view) {
if (view.getId() == R.id.download_remainder) {
onDownloadRemainder();
}
mController.loadMessageForViewRemote(mAccount, mMessageReference.folderName, mMessageReference.uid,
downloadMessageListener);
}
private void setProgress(boolean enable) {
@ -515,6 +516,15 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
}
}
private String getSubjectForMessage(LocalMessage message) {
String subject = message.getSubject();
if (TextUtils.isEmpty(subject)) {
return mContext.getString(R.string.general_no_subject);
}
return subject;
}
public void moveMessage(MessageReference reference, String destFolderName) {
mController.moveMessage(mAccount, mMessageReference.folderName, mMessage,
destFolderName, null);
@ -525,202 +535,6 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
destFolderName, null);
}
class Listener extends MessagingListener {
@Override
public void loadMessageForViewHeadersAvailable(final Account account, String folder, String uid,
final Message message) {
if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder)
|| !mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
/*
* Clone the message object because the original could be modified by
* MessagingController later. This could lead to a ConcurrentModificationException
* when that same object is accessed by the UI thread (below).
*
* See issue 3953
*
* This is just an ugly hack to get rid of the most pressing problem. A proper way to
* fix this is to make Message thread-safe. Or, even better, rewriting the UI code to
* access messages via a ContentProvider.
*
*/
final Message clonedMessage = message.clone();
mHandler.post(new Runnable() {
@Override
public void run() {
if (!clonedMessage.isSet(Flag.X_DOWNLOADED_FULL) &&
!clonedMessage.isSet(Flag.X_DOWNLOADED_PARTIAL)) {
String text = mContext.getString(R.string.message_view_downloading);
mMessageView.showStatusMessage(text);
}
mMessageView.setHeaders(clonedMessage, account);
final String subject = clonedMessage.getSubject();
if (subject == null || subject.equals("")) {
displayMessageSubject(mContext.getString(R.string.general_no_subject));
} else {
displayMessageSubject(clonedMessage.getSubject());
}
mMessageView.setOnFlagListener(new OnClickListener() {
@Override
public void onClick(View v) {
onToggleFlagged();
}
});
}
});
}
@Override
public void loadMessageForViewBodyAvailable(final Account account, String folder,
String uid, final Message message) {
if (!(message instanceof LocalMessage) ||
!mMessageReference.uid.equals(uid) ||
!mMessageReference.folderName.equals(folder) ||
!mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
try {
mMessage = (LocalMessage) message;
mMessageView.setMessage(account, (LocalMessage) message, mPgpData,
mController, mListener);
mFragmentListener.updateMenu();
} catch (MessagingException e) {
Log.v(K9.LOG_TAG, "loadMessageForViewBodyAvailable", e);
}
}
});
}
@Override
public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) {
if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder)
|| !mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
setProgress(false);
if (t instanceof IllegalArgumentException) {
mHandler.invalidIdError();
} else {
mHandler.networkError();
}
if (mMessage == null || mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL)) {
mMessageView.showStatusMessage(
mContext.getString(R.string.webview_empty_message));
}
}
});
}
@Override
public void loadMessageForViewFinished(Account account, String folder, String uid, final Message message) {
if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder)
|| !mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
setProgress(false);
mMessageView.setShowDownloadButton(message);
}
});
}
@Override
public void loadMessageForViewStarted(Account account, String folder, String uid) {
if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder)
|| !mMessageReference.accountUuid.equals(account.getUuid())) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
setProgress(true);
}
});
}
@Override
public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, final boolean requiresDownload) {
if (mMessage != message) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
mMessageView.setAttachmentsEnabled(false);
showDialog(R.id.dialog_attachment_progress);
if (requiresDownload) {
mHandler.fetchingAttachment();
}
}
});
}
@Override
public void loadAttachmentFinished(Account account, Message message, Part part, final Object tag) {
if (mMessage != message) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
mMessageView.setAttachmentsEnabled(true);
removeDialog(R.id.dialog_attachment_progress);
Object[] params = (Object[]) tag;
boolean download = (Boolean) params[0];
AttachmentView attachment = (AttachmentView) params[1];
if (download) {
attachment.writeFile();
} else {
attachment.showFile();
}
}
});
}
@Override
public void loadAttachmentFailed(Account account, Message message, Part part, Object tag, String reason) {
if (mMessage != message) {
return;
}
mHandler.post(new Runnable() {
@Override
public void run() {
mMessageView.setAttachmentsEnabled(true);
removeDialog(R.id.dialog_attachment_progress);
mHandler.networkError();
}
});
}
}
/**
* Used by MessageOpenPgpView
*/
public void setMessageWithOpenPgp(String decryptedData, OpenPgpSignatureResult signatureResult) {
try {
// TODO: get rid of PgpData?
PgpData data = new PgpData();
data.setDecryptedData(decryptedData);
data.setSignatureResult(signatureResult);
mMessageView.setMessage(mAccount, (LocalMessage) mMessage, data, mController, mListener);
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "displayMessageBody failed", e);
}
}
private void showDialog(int dialogId) {
DialogFragment fragment;
switch (dialogId) {
@ -782,7 +596,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
}
public void zoom(KeyEvent event) {
mMessageView.zoom(event);
// mMessageView.zoom(event);
}
@Override
@ -845,6 +659,52 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
}
}
public Context getContext() {
return mContext;
}
public void disableAttachmentButtons(AttachmentViewInfo attachment) {
// mMessageView.disableAttachmentButtons(attachment);
}
public void enableAttachmentButtons(AttachmentViewInfo attachment) {
// mMessageView.enableAttachmentButtons(attachment);
}
public void runOnMainThread(Runnable runnable) {
handler.post(runnable);
}
public void showAttachmentLoadingDialog() {
// mMessageView.disableAttachmentButtons();
showDialog(R.id.dialog_attachment_progress);
}
public void hideAttachmentLoadingDialogOnMainThread() {
handler.post(new Runnable() {
@Override
public void run() {
removeDialog(R.id.dialog_attachment_progress);
// mMessageView.enableAttachmentButtons();
}
});
}
public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) {
// mMessageView.refreshAttachmentThumbnail(attachment);
}
@Override
public void onPgpSignatureButtonClick(PendingIntent pendingIntent) {
try {
getActivity().startIntentSenderForResult(
pendingIntent.getIntentSender(),
42, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(K9.LOG_TAG, "SendIntentException", e);
}
}
public interface MessageViewFragmentListener {
public void onForward(LocalMessage mMessage, PgpData mPgpData);
public void disableDeleteAction();
@ -861,7 +721,105 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
return mInitialized ;
}
public LayoutInflater getFragmentLayoutInflater() {
return mLayoutInflater;
class LocalMessageLoaderCallback implements LoaderCallbacks<LocalMessage> {
@Override
public Loader<LocalMessage> onCreateLoader(int id, Bundle args) {
setProgress(true);
return new LocalMessageLoader(mContext, mController, mAccount, mMessageReference);
}
@Override
public void onLoadFinished(Loader<LocalMessage> loader, LocalMessage message) {
setProgress(false);
mMessage = message;
if (message == null) {
onLoadMessageFromDatabaseFailed();
} else {
onLoadMessageFromDatabaseFinished(message);
}
}
@Override
public void onLoaderReset(Loader<LocalMessage> loader) {
// Do nothing
}
}
class DecodeMessageLoaderCallback implements LoaderCallbacks<MessageViewInfo> {
@Override
public Loader<MessageViewInfo> onCreateLoader(int id, Bundle args) {
setProgress(true);
return new DecodeMessageLoader(mContext, mMessage, messageAnnotations);
}
@Override
public void onLoadFinished(Loader<MessageViewInfo> loader, MessageViewInfo messageContainer) {
setProgress(false);
onDecodeMessageFinished(messageContainer);
}
@Override
public void onLoaderReset(Loader<MessageViewInfo> loader) {
// Do nothing
}
}
@Override
public void onViewAttachment(AttachmentViewInfo attachment) {
//TODO: check if we have to download the attachment first
getAttachmentController(attachment).viewAttachment();
}
@Override
public void onSaveAttachment(AttachmentViewInfo attachment) {
//TODO: check if we have to download the attachment first
getAttachmentController(attachment).saveAttachment();
}
@Override
public void onSaveAttachmentToUserProvidedDirectory(final AttachmentViewInfo attachment) {
//TODO: check if we have to download the attachment first
currentAttachmentViewInfo = attachment;
FileBrowserHelper.getInstance().showFileBrowserActivity(MessageViewFragment.this, null,
ACTIVITY_CHOOSE_DIRECTORY, new FileBrowserFailOverCallback() {
@Override
public void onPathEntered(String path) {
getAttachmentController(attachment).saveAttachmentTo(path);
}
@Override
public void onCancel() {
// Do nothing
}
});
}
private AttachmentController getAttachmentController(AttachmentViewInfo attachment) {
return new AttachmentController(mController, this, attachment);
}
private class DownloadMessageListener extends MessagingListener {
@Override
public void loadMessageForViewFinished(Account account, String folder, String uid, final LocalMessage message) {
handler.post(new Runnable() {
@Override
public void run() {
onMessageDownloadFinished(message);
}
});
}
@Override
public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) {
handler.post(new Runnable() {
@Override
public void run() {
onDownloadMessageFailed(t);
}
});
}
}
}

View File

@ -0,0 +1,363 @@
package com.fsck.k9.ui.messageview;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.fsck.k9.R;
import com.fsck.k9.mailstore.OpenPgpResultAnnotation;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpUtils;
public class OpenPgpHeaderView extends LinearLayout {
private Context context;
private OpenPgpHeaderViewCallback callback;
private OpenPgpResultAnnotation cryptoAnnotation;
private ImageView resultEncryptionIcon;
private TextView resultEncryptionText;
private ImageView resultSignatureIcon;
private TextView resultSignatureText;
private LinearLayout resultSignatureLayout;
private TextView resultSignatureName;
private TextView resultSignatureEmail;
private Button resultSignatureButton;
public OpenPgpHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
@Override
public void onFinishInflate() {
resultEncryptionIcon = (ImageView) findViewById(R.id.result_encryption_icon);
resultEncryptionText = (TextView) findViewById(R.id.result_encryption_text);
resultSignatureIcon = (ImageView) findViewById(R.id.result_signature_icon);
resultSignatureText = (TextView) findViewById(R.id.result_signature_text);
resultSignatureLayout = (LinearLayout) findViewById(R.id.result_signature_layout);
resultSignatureName = (TextView) findViewById(R.id.result_signature_name);
resultSignatureEmail = (TextView) findViewById(R.id.result_signature_email);
resultSignatureButton = (Button) findViewById(R.id.result_signature_button);
}
public void setCallback(OpenPgpHeaderViewCallback callback) {
this.callback = callback;
}
public void setOpenPgpData(OpenPgpResultAnnotation cryptoAnnotation) {
this.cryptoAnnotation = cryptoAnnotation;
initializeEncryptionHeader();
initializeSignatureHeader();
}
private void initializeEncryptionHeader() {
if (noCryptoAnnotationFound()) {
displayNotEncrypted();
return;
}
switch (cryptoAnnotation.getErrorType()) {
case NONE: {
if (cryptoAnnotation.wasEncrypted()) {
displayEncrypted();
} else {
displayNotEncrypted();
}
break;
}
case CRYPTO_API_RETURNED_ERROR: {
displayEncryptionError();
break;
}
case ENCRYPTED_BUT_INCOMPLETE: {
displayIncompleteEncryptedPart();
break;
}
case SIGNED_BUT_INCOMPLETE: {
displayNotEncrypted();
break;
}
}
}
private boolean noCryptoAnnotationFound() {
return cryptoAnnotation == null;
}
private void displayEncrypted() {
setEncryptionImageAndTextColor(CryptoState.ENCRYPTED);
resultEncryptionText.setText(R.string.openpgp_result_encrypted);
}
private void displayNotEncrypted() {
setEncryptionImageAndTextColor(CryptoState.NOT_ENCRYPTED);
resultEncryptionText.setText(R.string.openpgp_result_not_encrypted);
}
private void displayEncryptionError() {
setEncryptionImageAndTextColor(CryptoState.INVALID);
OpenPgpError error = cryptoAnnotation.getError();
String text;
if (error == null) {
text = context.getString(R.string.openpgp_unknown_error);
} else {
text = context.getString(R.string.openpgp_error, error.getMessage());
}
resultEncryptionText.setText(text);
}
private void displayIncompleteEncryptedPart() {
setEncryptionImageAndTextColor(CryptoState.UNAVAILABLE);
resultEncryptionText.setText(R.string.crypto_incomplete_message);
}
private void initializeSignatureHeader() {
initializeSignatureButton();
if (noCryptoAnnotationFound()) {
displayNotSigned();
return;
}
switch (cryptoAnnotation.getErrorType()) {
case CRYPTO_API_RETURNED_ERROR:
case NONE: {
displayVerificationResult();
break;
}
case ENCRYPTED_BUT_INCOMPLETE:
case SIGNED_BUT_INCOMPLETE: {
displayIncompleteSignedPart();
break;
}
}
}
private void displayIncompleteSignedPart() {
setSignatureImageAndTextColor(CryptoState.UNAVAILABLE);
resultSignatureText.setText(R.string.crypto_incomplete_message);
hideSignatureLayout();
}
private void displayVerificationResult() {
OpenPgpSignatureResult signatureResult = cryptoAnnotation.getSignatureResult();
if (signatureResult == null) {
displayNotSigned();
return;
}
switch (signatureResult.getStatus()) {
case OpenPgpSignatureResult.SIGNATURE_ERROR: {
displaySignatureError();
break;
}
case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED: {
displaySignatureSuccessCertified();
break;
}
case OpenPgpSignatureResult.SIGNATURE_KEY_MISSING: {
displaySignatureKeyMissing();
break;
}
case OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED: {
displaySignatureSuccessUncertified();
break;
}
case OpenPgpSignatureResult.SIGNATURE_KEY_EXPIRED: {
displaySignatureKeyExpired();
break;
}
case OpenPgpSignatureResult.SIGNATURE_KEY_REVOKED: {
displaySignatureKeyRevoked();
break;
}
}
}
private void initializeSignatureButton() {
if (noCryptoAnnotationFound()) {
hideSignatureButton();
} else if (isSignatureButtonUsed()) {
setSignatureButtonClickListener();
} else {
hideSignatureButton();
}
}
private boolean isSignatureButtonUsed() {
return cryptoAnnotation.getPendingIntent() != null;
}
private void setSignatureButtonClickListener() {
final PendingIntent pendingIntent = cryptoAnnotation.getPendingIntent();
resultSignatureButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
callback.onPgpSignatureButtonClick(pendingIntent);
}
});
}
private void hideSignatureButton() {
resultSignatureButton.setVisibility(View.GONE);
resultSignatureButton.setOnClickListener(null);
}
private void showSignatureButtonWithTextIfNecessary(@StringRes int stringId) {
if (isSignatureButtonUsed()) {
resultSignatureButton.setVisibility(View.VISIBLE);
resultSignatureButton.setText(stringId);
}
}
private void displayNotSigned() {
setSignatureImageAndTextColor(CryptoState.NOT_SIGNED);
resultSignatureText.setText(R.string.openpgp_result_no_signature);
hideSignatureLayout();
}
private void displaySignatureError() {
setSignatureImageAndTextColor(CryptoState.INVALID);
resultSignatureText.setText(R.string.openpgp_result_invalid_signature);
hideSignatureLayout();
}
private void displaySignatureSuccessCertified() {
setSignatureImageAndTextColor(CryptoState.VERIFIED);
resultSignatureText.setText(R.string.openpgp_result_signature_certified);
displayUserIdAndSignatureButton();
}
private void displaySignatureKeyMissing() {
setSignatureImageAndTextColor(CryptoState.UNKNOWN_KEY);
resultSignatureText.setText(R.string.openpgp_result_signature_missing_key);
setUserId(cryptoAnnotation.getSignatureResult());
showSignatureButtonWithTextIfNecessary(R.string.openpgp_result_action_lookup);
showSignatureLayout();
}
private void displaySignatureSuccessUncertified() {
setSignatureImageAndTextColor(CryptoState.UNVERIFIED);
resultSignatureText.setText(R.string.openpgp_result_signature_uncertified);
displayUserIdAndSignatureButton();
}
private void displaySignatureKeyExpired() {
setSignatureImageAndTextColor(CryptoState.EXPIRED);
resultSignatureText.setText(R.string.openpgp_result_signature_expired_key);
displayUserIdAndSignatureButton();
}
private void displaySignatureKeyRevoked() {
setSignatureImageAndTextColor(CryptoState.REVOKED);
resultSignatureText.setText(R.string.openpgp_result_signature_revoked_key);
displayUserIdAndSignatureButton();
}
private void displayUserIdAndSignatureButton() {
setUserId(cryptoAnnotation.getSignatureResult());
showSignatureButtonWithTextIfNecessary(R.string.openpgp_result_action_show);
showSignatureLayout();
}
private void setUserId(OpenPgpSignatureResult signatureResult) {
final OpenPgpUtils.UserId userInfo = OpenPgpUtils.splitUserId(signatureResult.getPrimaryUserId());
if (userInfo.name != null) {
resultSignatureName.setText(userInfo.name);
} else {
resultSignatureName.setText(R.string.openpgp_result_no_name);
}
if (userInfo.email != null) {
resultSignatureEmail.setText(userInfo.email);
} else {
resultSignatureEmail.setText(R.string.openpgp_result_no_email);
}
}
private void hideSignatureLayout() {
resultSignatureLayout.setVisibility(View.GONE);
}
private void showSignatureLayout() {
resultSignatureLayout.setVisibility(View.VISIBLE);
}
private void setEncryptionImageAndTextColor(CryptoState state) {
setStatusImageAndTextColor(resultEncryptionIcon, resultEncryptionText, state);
}
private void setSignatureImageAndTextColor(CryptoState state) {
setStatusImageAndTextColor(resultSignatureIcon, resultSignatureText, state);
}
private void setStatusImageAndTextColor(ImageView statusIcon, TextView statusText, CryptoState state) {
Drawable statusImageDrawable = context.getResources().getDrawable(state.getDrawableId());
statusIcon.setImageDrawable(statusImageDrawable);
int color = context.getResources().getColor(state.getColorId());
statusIcon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
if (statusText != null) {
statusText.setTextColor(color);
}
}
private enum CryptoState {
VERIFIED(R.drawable.status_signature_verified_cutout, R.color.openpgp_green),
ENCRYPTED(R.drawable.status_lock_closed, R.color.openpgp_green),
UNAVAILABLE(R.drawable.status_signature_unverified_cutout, R.color.openpgp_orange),
UNVERIFIED(R.drawable.status_signature_unverified_cutout, R.color.openpgp_orange),
UNKNOWN_KEY(R.drawable.status_signature_unknown_cutout, R.color.openpgp_orange),
REVOKED(R.drawable.status_signature_revoked_cutout, R.color.openpgp_red),
EXPIRED(R.drawable.status_signature_expired_cutout, R.color.openpgp_red),
NOT_ENCRYPTED(R.drawable.status_lock_open, R.color.openpgp_red),
NOT_SIGNED(R.drawable.status_signature_unknown_cutout, R.color.openpgp_red),
INVALID(R.drawable.status_signature_invalid_cutout, R.color.openpgp_red);
private final int drawableId;
private final int colorId;
CryptoState(@DrawableRes int drawableId, @ColorRes int colorId) {
this.drawableId = drawableId;
this.colorId = colorId;
}
@DrawableRes
public int getDrawableId() {
return drawableId;
}
@ColorRes
public int getColorId() {
return colorId;
}
}
}

View File

@ -0,0 +1,9 @@
package com.fsck.k9.ui.messageview;
import android.app.PendingIntent;
interface OpenPgpHeaderViewCallback {
void onPgpSignatureButtonClick(PendingIntent pendingIntent);
}

View File

@ -0,0 +1,6 @@
package com.fsck.k9.ui.messageview;
interface ShowPicturesController {
void notifyMessageContainerContainsPictures(MessageContainerView messageContainerView);
}

View File

@ -1,479 +0,0 @@
package com.fsck.k9.view;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.cache.TemporaryAttachmentStore;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.helper.FileHelper;
import com.fsck.k9.helper.MediaScannerNotifier;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.LocalAttachmentBodyPart;
import com.fsck.k9.provider.AttachmentProvider;
import org.apache.commons.io.IOUtils;
public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener {
private Context context;
private Message message;
private LocalAttachmentBodyPart part;
private Account account;
private MessagingController controller;
private MessagingListener listener;
private AttachmentFileDownloadCallback callback;
private Button viewButton;
private Button downloadButton;
private String name;
private String contentType;
private long size;
public AttachmentView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.context = context;
}
public AttachmentView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public AttachmentView(Context context) {
super(context);
this.context = context;
}
public void setButtonsEnabled(boolean enabled) {
viewButton.setEnabled(enabled);
downloadButton.setEnabled(enabled);
}
/**
* Populates this view with information about the attachment.
* <p>
* This method also decides which attachments are displayed when the "show attachments" button
* is pressed, and which attachments are only displayed after the "show more attachments"
* button was pressed.<br>
* Inline attachments with content ID and unnamed attachments fall into the second category.
* </p>
*
* @return {@code true} for a regular attachment. {@code false} for attachments that should be initially hidden.
*/
public boolean populateFromPart(Part inputPart, Message message, Account account,
MessagingController controller, MessagingListener listener) throws MessagingException {
part = (LocalAttachmentBodyPart) inputPart;
this.message = message;
this.account = account;
this.controller = controller;
this.listener = listener;
boolean firstClassAttachment = extractAttachmentInformation(part);
displayAttachmentInformation();
return firstClassAttachment;
}
//TODO: extract this code to a helper class
private boolean extractAttachmentInformation(Part part) throws MessagingException {
boolean firstClassAttachment = true;
contentType = part.getMimeType();
String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType());
String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
name = MimeUtility.getHeaderParameter(contentTypeHeader, "name");
if (name == null) {
name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
}
if (name == null) {
firstClassAttachment = false;
String extension = MimeUtility.getExtensionByMimeType(contentType);
name = "noname" + ((extension != null) ? "." + extension : "");
}
// Inline parts with a content-id are almost certainly components of an HTML message
// not attachments. Only show them if the user pressed the button to show more
// attachments.
if (contentDisposition != null &&
MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)")
&& part.getHeader(MimeHeader.HEADER_CONTENT_ID) != null) {
firstClassAttachment = false;
}
String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size");
if (sizeParam != null) {
try {
size = Integer.parseInt(sizeParam);
} catch (NumberFormatException e) { /* ignore */ }
}
return firstClassAttachment;
}
private void displayAttachmentInformation() {
TextView attachmentName = (TextView) findViewById(R.id.attachment_name);
TextView attachmentInfo = (TextView) findViewById(R.id.attachment_info);
viewButton = (Button) findViewById(R.id.view);
downloadButton = (Button) findViewById(R.id.download);
if (size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
viewButton.setVisibility(View.GONE);
downloadButton.setVisibility(View.GONE);
}
viewButton.setOnClickListener(this);
downloadButton.setOnClickListener(this);
downloadButton.setOnLongClickListener(this);
attachmentName.setText(name);
attachmentInfo.setText(SizeFormatter.formatSize(context, size));
ImageView thumbnail = (ImageView) findViewById(R.id.attachment_icon);
new LoadAndDisplayThumbnailAsyncTask(thumbnail).execute();
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.view: {
onViewButtonClicked();
break;
}
case R.id.download: {
onSaveButtonClicked();
break;
}
}
}
@Override
public boolean onLongClick(View view) {
if (view.getId() == R.id.download) {
callback.pickDirectoryToSaveAttachmentTo(this);
return true;
}
return false;
}
private void onViewButtonClicked() {
if (message != null) {
controller.loadAttachment(account, message, part, new Object[] {false, this}, listener);
}
}
private void onSaveButtonClicked() {
boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
if (!isExternalStorageMounted) {
String message = context.getString(R.string.message_view_status_attachment_not_saved);
displayMessageToUser(message);
return;
}
if (message != null) {
controller.loadAttachment(account, message, part, new Object[] {true, this}, listener);
}
}
public void writeFile() {
writeFile(new File(K9.getAttachmentDefaultPath()));
}
/**
* Saves the attachment as file in the given directory
*/
public void writeFile(File directory) {
try {
File file = saveAttachmentWithUniqueFileName(directory);
displayAttachmentSavedMessage(file.toString());
MediaScannerNotifier.notify(context, file);
} catch (IOException ioe) {
if (K9.DEBUG) {
Log.e(K9.LOG_TAG, "Error saving attachment", ioe);
}
displayAttachmentNotSavedMessage();
}
}
private File saveAttachmentWithUniqueFileName(File directory) throws IOException {
String filename = FileHelper.sanitizeFilename(name);
File file = FileHelper.createUniqueFile(directory, filename);
writeAttachmentToStorage(file);
return file;
}
private void writeAttachmentToStorage(File file) throws IOException {
Uri uri = AttachmentProvider.getAttachmentUri(account, part.getAttachmentId());
InputStream in = context.getContentResolver().openInputStream(uri);
try {
OutputStream out = new FileOutputStream(file);
try {
IOUtils.copy(in, out);
out.flush();
} finally {
out.close();
}
} finally {
in.close();
}
}
public void showFile() {
new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private Intent getBestViewIntentAndSaveFileIfNecessary() {
String inferredMimeType = MimeUtility.getMimeTypeByExtension(name);
IntentAndResolvedActivitiesCount resolvedIntentInfo;
if (MimeUtility.isDefaultMimeType(contentType)) {
resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType);
} else {
resolvedIntentInfo = getBestViewIntentForMimeType(contentType);
if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(contentType)) {
resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType);
}
}
if (!resolvedIntentInfo.hasResolvedActivities()) {
resolvedIntentInfo = getBestViewIntentForMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE);
}
Intent viewIntent;
if (resolvedIntentInfo.hasResolvedActivities() && resolvedIntentInfo.containsFileUri()) {
try {
File tempFile = TemporaryAttachmentStore.getFileForWriting(context, name);
writeAttachmentToStorage(tempFile);
viewIntent = createViewIntentForFileUri(resolvedIntentInfo.getMimeType(), Uri.fromFile(tempFile));
} catch (IOException e) {
if (K9.DEBUG) {
Log.e(K9.LOG_TAG, "Error while saving attachment to use file:// URI with ACTION_VIEW Intent", e);
}
viewIntent = createViewIntentForAttachmentProviderUri(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE);
}
} else {
viewIntent = resolvedIntentInfo.getIntent();
}
return viewIntent;
}
private IntentAndResolvedActivitiesCount getBestViewIntentForMimeType(String mimeType) {
Intent contentUriIntent = createViewIntentForAttachmentProviderUri(mimeType);
int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent);
if (contentUriActivitiesCount > 0) {
return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
}
File tempFile = TemporaryAttachmentStore.getFile(context, name);
Uri tempFileUri = Uri.fromFile(tempFile);
Intent fileUriIntent = createViewIntentForFileUri(mimeType, tempFileUri);
int fileUriActivitiesCount = getResolvedIntentActivitiesCount(fileUriIntent);
if (fileUriActivitiesCount > 0) {
return new IntentAndResolvedActivitiesCount(fileUriIntent, fileUriActivitiesCount);
}
return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
}
private Intent createViewIntentForAttachmentProviderUri(String mimeType) {
Uri uri = AttachmentProvider.getAttachmentUriForViewing(account, part.getAttachmentId(), mimeType, name);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
addUiIntentFlags(intent);
return intent;
}
private Intent createViewIntentForFileUri(String mimeType, Uri uri) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
addUiIntentFlags(intent);
return intent;
}
private void addUiIntentFlags(Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
}
private int getResolvedIntentActivitiesCount(Intent intent) {
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> resolveInfos =
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
return resolveInfos.size();
}
private void displayAttachmentSavedMessage(final String filename) {
String message = context.getString(R.string.message_view_status_attachment_saved, filename);
displayMessageToUser(message);
}
private void displayAttachmentNotSavedMessage() {
String message = context.getString(R.string.message_view_status_attachment_not_saved);
displayMessageToUser(message);
}
private void displayMessageToUser(String message) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
public void setCallback(AttachmentFileDownloadCallback callback) {
this.callback = callback;
}
public interface AttachmentFileDownloadCallback {
/**
* This method is called to ask the user to pick a directory to save the attachment to.
* <p/>
* After the user has selected a directory, the implementation of this interface has to call
* {@link #writeFile(File)} on the object supplied as argument in order for the attachment to be saved.
*/
public void pickDirectoryToSaveAttachmentTo(AttachmentView caller);
}
private static class IntentAndResolvedActivitiesCount {
private Intent intent;
private int activitiesCount;
IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) {
this.intent = intent;
this.activitiesCount = activitiesCount;
}
public Intent getIntent() {
return intent;
}
public boolean hasResolvedActivities() {
return activitiesCount > 0;
}
public String getMimeType() {
return intent.getType();
}
public boolean containsFileUri() {
return "file".equals(intent.getData().getScheme());
}
}
private class LoadAndDisplayThumbnailAsyncTask extends AsyncTask<Void, Void, Bitmap> {
private final ImageView thumbnail;
public LoadAndDisplayThumbnailAsyncTask(ImageView thumbnail) {
this.thumbnail = thumbnail;
}
protected Bitmap doInBackground(Void... asyncTaskArgs) {
return getPreviewIcon();
}
private Bitmap getPreviewIcon() {
Bitmap icon = null;
try {
InputStream input = context.getContentResolver().openInputStream(
AttachmentProvider.getAttachmentThumbnailUri(account,
part.getAttachmentId(),
62,
62));
icon = BitmapFactory.decodeStream(input);
input.close();
} catch (Exception e) {
// We don't care what happened, we just return null for the preview icon.
}
return icon;
}
protected void onPostExecute(Bitmap previewIcon) {
if (previewIcon != null) {
thumbnail.setImageBitmap(previewIcon);
} else {
thumbnail.setImageResource(R.drawable.attached_image_placeholder);
}
}
}
private class ViewAttachmentAsyncTask extends AsyncTask<Void, Void, Intent> {
@Override
protected void onPreExecute() {
viewButton.setEnabled(false);
}
@Override
protected Intent doInBackground(Void... params) {
return getBestViewIntentAndSaveFileIfNecessary();
}
@Override
protected void onPostExecute(Intent intent) {
viewAttachment(intent);
viewButton.setEnabled(true);
}
private void viewAttachment(Intent intent) {
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(K9.LOG_TAG, "Could not display attachment of type " + contentType, e);
String message = context.getString(R.string.message_view_no_viewer, contentType);
displayMessageToUser(message);
}
}
}
}

View File

@ -0,0 +1,125 @@
package com.fsck.k9.view;
import java.io.InputStream;
import java.util.Stack;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mailstore.AttachmentViewInfo;
import com.fsck.k9.mailstore.LocalMessageExtractor;
/**
* {@link WebViewClient} that intercepts requests for {@code cid:} URIs to load the respective body part.
*/
public abstract class K9WebViewClient extends WebViewClient {
private static final String CID_SCHEME = "cid";
private static final WebResourceResponse RESULT_DO_NOT_INTERCEPT = null;
private static final WebResourceResponse RESULT_DUMMY_RESPONSE = new WebResourceResponse(null, null, null);
public static WebViewClient newInstance(Part part) {
if (Build.VERSION.SDK_INT < 21) {
return new PreLollipopWebViewClient(part);
}
return new LollipopWebViewClient(part);
}
private final Part part;
private K9WebViewClient(Part part) {
this.part = part;
}
protected WebResourceResponse shouldInterceptRequest(WebView webView, Uri uri) {
if (!CID_SCHEME.equals(uri.getScheme())) {
return RESULT_DO_NOT_INTERCEPT;
}
String cid = uri.getSchemeSpecificPart();
if (TextUtils.isEmpty(cid)) {
return RESULT_DUMMY_RESPONSE;
}
Part part = getPartForContentId(cid);
if (part == null) {
return RESULT_DUMMY_RESPONSE;
}
Context context = webView.getContext();
ContentResolver contentResolver = context.getContentResolver();
try {
AttachmentViewInfo attachmentInfo = LocalMessageExtractor.extractAttachmentInfo(context, part);
String mimeType = attachmentInfo.mimeType;
InputStream inputStream = contentResolver.openInputStream(attachmentInfo.uri);
return new WebResourceResponse(mimeType, null, inputStream);
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Error while intercepting URI: " + uri, e);
return RESULT_DUMMY_RESPONSE;
}
}
private Part getPartForContentId(String cid) {
Stack<Part> partsToCheck = new Stack<Part>();
partsToCheck.push(part);
while (!partsToCheck.isEmpty()) {
Part part = partsToCheck.pop();
Body body = part.getBody();
if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
for (Part bodyPart : multipart.getBodyParts()) {
partsToCheck.push(bodyPart);
}
} else if (cid.equals(part.getContentId())) {
return part;
}
}
return null;
}
private static class PreLollipopWebViewClient extends K9WebViewClient {
protected PreLollipopWebViewClient(Part part) {
super(part);
}
@SuppressWarnings("deprecation")
@Override
public WebResourceResponse shouldInterceptRequest(WebView webView, String url) {
return shouldInterceptRequest(webView, Uri.parse(url));
}
}
@TargetApi(VERSION_CODES.LOLLIPOP)
private static class LollipopWebViewClient extends K9WebViewClient {
protected LollipopWebViewClient(Part part) {
super(part);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) {
return shouldInterceptRequest(webView, request.getUrl());
}
}
}

View File

@ -153,8 +153,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener {
}
public void setOnFlagListener(OnClickListener listener) {
if (mFlagged == null)
return;
mFlagged.setOnClickListener(listener);
}

View File

@ -1,424 +0,0 @@
package com.fsck.k9.view;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import android.app.Activity;
import android.app.Fragment;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.crypto.CryptoHelper;
import com.fsck.k9.crypto.OpenPgpApiHelper;
import com.fsck.k9.fragment.MessageViewFragment;
import com.fsck.k9.helper.IdentityHelper;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeUtility;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
public class MessageOpenPgpView extends LinearLayout {
private Context mContext;
private MessageViewFragment mFragment;
private RelativeLayout mSignatureLayout = null;
private ImageView mSignatureStatusImage = null;
private TextView mSignatureUserId = null;
private TextView mText = null;
private ProgressBar mProgress;
private Button mGetKeyButton;
private OpenPgpServiceConnection mOpenPgpServiceConnection;
private OpenPgpApi mOpenPgpApi;
private String mOpenPgpProvider;
private Message mMessage;
private PendingIntent mMissingKeyPI;
private static final int REQUEST_CODE_DECRYPT_VERIFY = 12;
String mData;
Account mAccount;
public MessageOpenPgpView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
public void setupChildViews() {
mSignatureLayout = (RelativeLayout) findViewById(R.id.openpgp_signature_layout);
mSignatureStatusImage = (ImageView) findViewById(R.id.openpgp_signature_status);
mSignatureUserId = (TextView) findViewById(R.id.openpgp_user_id);
mText = (TextView) findViewById(R.id.openpgp_text);
mProgress = (ProgressBar) findViewById(R.id.openpgp_progress);
mGetKeyButton = (Button) findViewById(R.id.openpgp_get_key);
mGetKeyButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
getMissingKey();
}
});
}
public void setFragment(Fragment fragment) {
mFragment = (MessageViewFragment) fragment;
}
/**
* Fill the decrypt layout with signature data, if known, make controls
* visible, if they should be visible.
*/
public void updateLayout(Account account, String decryptedData,
final OpenPgpSignatureResult signatureResult,
final Message message) {
// set class variables
mAccount = account;
mOpenPgpProvider = mAccount.getOpenPgpProvider();
mMessage = message;
// only use this view if a OpenPGP Provider is set
if (mOpenPgpProvider == null) {
return;
}
Activity activity = mFragment.getActivity();
if (activity == null) {
return;
}
// bind to service
mOpenPgpServiceConnection = new OpenPgpServiceConnection(activity,
mOpenPgpProvider);
mOpenPgpServiceConnection.bindToService();
if ((message == null) && (decryptedData == null)) {
this.setVisibility(View.GONE);
// don't process further
return;
}
if (decryptedData != null && signatureResult == null) {
// encrypted-only
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_blue));
mText.setText(R.string.openpgp_successful_decryption);
// don't process further
return;
} else if (decryptedData != null && signatureResult != null) {
// signed-only and signed-and-encrypted
switch (signatureResult.getStatus()) {
case OpenPgpSignatureResult.SIGNATURE_ERROR:
// TODO: signature error but decryption works?
mText.setText(R.string.openpgp_signature_invalid);
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_red));
mGetKeyButton.setVisibility(View.GONE);
mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
mSignatureLayout.setVisibility(View.GONE);
break;
case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED:
if (signatureResult.isSignatureOnly()) {
mText.setText(R.string.openpgp_signature_valid_certified);
}
else {
mText.setText(R.string.openpgp_successful_decryption_valid_signature_certified);
}
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_green));
mGetKeyButton.setVisibility(View.GONE);
mSignatureUserId.setText(signatureResult.getPrimaryUserId());
mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
mSignatureLayout.setVisibility(View.VISIBLE);
break;
case OpenPgpSignatureResult.SIGNATURE_KEY_MISSING:
if (signatureResult.isSignatureOnly()) {
mText.setText(R.string.openpgp_signature_unknown_text);
}
else {
mText.setText(R.string.openpgp_successful_decryption_unknown_signature);
}
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_orange));
mGetKeyButton.setVisibility(View.VISIBLE);
mSignatureUserId.setText(R.string.openpgp_signature_unknown);
mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
mSignatureLayout.setVisibility(View.VISIBLE);
break;
case OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED:
if (signatureResult.isSignatureOnly()) {
mText.setText(R.string.openpgp_signature_valid_uncertified);
}
else {
mText.setText(R.string.openpgp_successful_decryption_valid_signature_uncertified);
}
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_orange));
mGetKeyButton.setVisibility(View.GONE);
mSignatureUserId.setText(signatureResult.getPrimaryUserId());
mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
mSignatureLayout.setVisibility(View.VISIBLE);
break;
default:
break;
}
// don't process further
return;
}
// Start new decryption/verification
CryptoHelper helper = new CryptoHelper();
if (helper.isEncrypted(message) || helper.isSigned(message)) {
// start automatic decrypt
decryptAndVerify(message);
} else {
try {
// check for PGP/MIME encryption
Part pgp = MimeUtility.findFirstPartByMimeType(message, "application/pgp-encrypted");
if (pgp != null) {
Toast.makeText(mContext, R.string.pgp_mime_unsupported, Toast.LENGTH_LONG)
.show();
}
} catch (MessagingException e) {
// nothing to do...
}
}
}
private void decryptAndVerify(final Message message) {
this.setVisibility(View.VISIBLE);
mProgress.setVisibility(View.VISIBLE);
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_orange));
mText.setText(R.string.openpgp_decrypting_verifying);
// waiting in a new thread
Runnable r = new Runnable() {
@Override
public void run() {
try {
// get data String
Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
if (part == null) {
part = MimeUtility.findFirstPartByMimeType(message, "text/html");
}
if (part != null) {
mData = MessageExtractor.getTextFromPart(part);
}
// wait for service to be bound
while (!mOpenPgpServiceConnection.isBound()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
mOpenPgpApi = new OpenPgpApi(getContext(),
mOpenPgpServiceConnection.getService());
decryptVerify(new Intent());
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Unable to decrypt email.", me);
}
}
};
new Thread(r).start();
}
private void decryptVerify(Intent intent) {
intent.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
intent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
Identity identity = IdentityHelper.getRecipientIdentityFromMessage(mAccount, mMessage);
String accName = OpenPgpApiHelper.buildAccountName(identity);
intent.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, accName);
InputStream is = new ByteArrayInputStream(mData.getBytes(Charset.forName("UTF-8")));
final ByteArrayOutputStream os = new ByteArrayOutputStream();
DecryptVerifyCallback callback = new DecryptVerifyCallback(os, REQUEST_CODE_DECRYPT_VERIFY);
mOpenPgpApi.executeApiAsync(intent, is, os, callback);
}
private void getMissingKey() {
try {
mFragment.getActivity().startIntentSenderForResult(
mMissingKeyPI.getIntentSender(),
REQUEST_CODE_DECRYPT_VERIFY, null, 0, 0, 0);
} catch (SendIntentException e) {
Log.e(K9.LOG_TAG, "SendIntentException", e);
}
}
/**
* Called on successful decrypt/verification
*/
private class DecryptVerifyCallback implements OpenPgpApi.IOpenPgpCallback {
ByteArrayOutputStream os;
int requestCode;
private DecryptVerifyCallback(ByteArrayOutputStream os, int requestCode) {
this.os = os;
this.requestCode = requestCode;
}
@Override
public void onReturn(Intent result) {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS: {
try {
final String output = os.toString("UTF-8");
OpenPgpSignatureResult sigResult = null;
if (result.hasExtra(OpenPgpApi.RESULT_SIGNATURE)) {
sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "result: " + os.toByteArray().length
+ " str=" + output);
// missing key -> PendingIntent to get keys
mMissingKeyPI = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
mProgress.setVisibility(View.GONE);
mFragment.setMessageWithOpenPgp(output, sigResult);
} catch (UnsupportedEncodingException e) {
Log.e(K9.LOG_TAG, "UnsupportedEncodingException", e);
}
break;
}
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
try {
mFragment.getActivity().startIntentSenderForResult(
pi.getIntentSender(),
requestCode, null, 0, 0, 0);
} catch (SendIntentException e) {
Log.e(K9.LOG_TAG, "SendIntentException", e);
}
break;
}
case OpenPgpApi.RESULT_CODE_ERROR: {
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
handleError(error);
break;
}
}
}
}
public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "onActivityResult resultCode: " + resultCode);
// try again after user interaction
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_DECRYPT_VERIFY) {
/*
* The data originally given to the decryptVerify() method, is again
* returned here to be used when calling decryptVerify() after user
* interaction. The Intent now also contains results from the user
* interaction, for example selected key ids.
*/
decryptVerify(data);
return true;
}
return false;
}
private void handleError(final OpenPgpError error) {
Activity activity = mFragment.getActivity();
if (activity == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
mProgress.setVisibility(View.GONE);
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "OpenPGP Error ID:" + error.getErrorId());
Log.d(K9.LOG_TAG, "OpenPGP Error Message:" + error.getMessage());
}
mText.setText(mFragment.getString(R.string.openpgp_error) + " "
+ error.getMessage());
MessageOpenPgpView.this.setBackgroundColor(mFragment.getResources().getColor(
R.color.openpgp_red));
}
});
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// bind to service if a OpenPGP Provider is available
if (mOpenPgpProvider != null) {
mOpenPgpServiceConnection = new OpenPgpServiceConnection(mFragment.getActivity(),
mOpenPgpProvider);
mOpenPgpServiceConnection.bindToService();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mOpenPgpServiceConnection != null) {
mOpenPgpServiceConnection.unbindFromService();
}
}
}

Some files were not shown because too many files have changed in this diff Show More