1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-23 09:52:16 -05:00

Merge remote-tracking branch 'upstream/master' into MessageReferenceImmutability

This commit is contained in:
Valentin CAULIER 2015-03-21 11:53:40 +01:00
commit ebcd10d1b1
225 changed files with 7498 additions and 4486 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 { 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() { public Multipart getParent() {
return mParent; return parent;
} }
public void setParent(Multipart parent) { public void setParent(Multipart parent) {
mParent = parent; this.parent = parent;
} }
public abstract void setEncoding(String encoding) throws MessagingException; public abstract void setEncoding(String encoding) throws MessagingException;

View File

@ -120,9 +120,6 @@ public abstract class Message implements Part, CompositeBody {
@Override @Override
public abstract Body getBody(); public abstract Body getBody();
@Override
public abstract String getContentType() throws MessagingException;
@Override @Override
public abstract void addHeader(String name, String value) throws MessagingException; 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; public abstract void removeHeader(String name) throws MessagingException;
@Override @Override
public abstract void setBody(Body body) throws MessagingException; public abstract void setBody(Body body);
public abstract long getId(); public abstract long getId();
@ -150,55 +147,6 @@ public abstract class Message implements Part, CompositeBody {
public abstract int getSize(); 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 {} public void delete(String trashFolderName) throws MessagingException {}
/* /*

View File

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

View File

@ -0,0 +1,25 @@
package com.fsck.k9.mail;
import android.net.ConnectivityManager;
/**
* Enum for some of
* https://developer.android.com/reference/android/net/ConnectivityManager.html#TYPE_MOBILE etc.
*/
public enum NetworkType {
WIFI,
MOBILE,
OTHER;
public static NetworkType fromConnectivityManagerType(int type){
switch (type) {
case ConnectivityManager.TYPE_MOBILE:
return MOBILE;
case ConnectivityManager.TYPE_WIFI:
return WIFI;
default:
return OTHER;
}
}
}

View File

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

View File

@ -17,7 +17,30 @@ import java.util.Map;
*/ */
public class ServerSettings { public class ServerSettings {
public enum Type { IMAP, SMTP, WebDAV, POP3 } public enum Type {
IMAP(143, 993),
SMTP(587, 465),
WebDAV(80, 443),
POP3(110, 995);
public final int defaultPort;
/**
* Note: port for connections using TLS (=SSL) immediately
* from the initial TCP connection.
*
* STARTTLS uses the defaultPort, then upgrades.
*
* See https://www.fastmail.com/help/technical/ssltlsstarttls.html.
*/
public final int defaultTlsPort;
private Type(int defaultPort, int defaultTlsPort) {
this.defaultPort = defaultPort;
this.defaultTlsPort = defaultTlsPort;
}
}
/** /**
* Name of the store or transport type (e.g. IMAP). * Name of the store or transport type (e.g. IMAP).

View File

@ -1,6 +1,5 @@
package com.fsck.k9.mail.internet; package com.fsck.k9.mail.internet;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.Base64OutputStream; import com.fsck.k9.mail.filter.Base64OutputStream;
import org.apache.commons.io.IOUtils; 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 * 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. * 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 static File mTempDirectory;
private File mFile; private File mFile;
@ -26,6 +25,10 @@ public class BinaryTempFileBody implements RawDataBody {
mTempDirectory = tempDirectory; mTempDirectory = tempDirectory;
} }
public static File getTempDirectory() {
return mTempDirectory;
}
@Override @Override
public String getEncoding() { public String getEncoding() {
return mEncoding; 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 { class BinaryTempFileBodyInputStream extends FilterInputStream {
public BinaryTempFileBodyInputStream(InputStream in) { public BinaryTempFileBodyInputStream(InputStream in) {
super(in); super(in);

View File

@ -11,10 +11,7 @@ import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
/** /**
* A {@link BinaryTempFileBody} extension containing a body of type * A {@link BinaryTempFileBody} extension containing a body of type message/rfc822.
* message/rfc822. This relates to a BinaryTempFileBody the same way that a
* {@link LocalAttachmentMessageBody} relates to a {@link LocalAttachmentBody}.
*
*/ */
public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody { 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. * determine the charset from HTML message.
*/ */
if (mimeType.equalsIgnoreCase("text/html") && charset == null) { if (mimeType.equalsIgnoreCase("text/html") && charset == null) {
InputStream in = part.getBody().getInputStream(); InputStream in = MimeUtility.decodeBody(body);
try { try {
byte[] buf = new byte[256]; byte[] buf = new byte[256];
in.read(buf, 0, buf.length); in.read(buf, 0, buf.length);
@ -64,18 +64,8 @@ public class MessageExtractor {
} }
} finally { } finally {
try { try {
if (in instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) { MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in);
/* } catch (IOException e) { /* ignore */ }
* 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 */ }
} }
} }
charset = fixupCharset(charset, getMessageFromPart(part)); charset = fixupCharset(charset, getMessageFromPart(part));
@ -84,22 +74,12 @@ public class MessageExtractor {
* Now we read the part into a buffer for further processing. Because * 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. * 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 { try {
String text = CharsetSupport.readToString(in, charset); return CharsetSupport.readToString(in, charset);
// Replace the body with a TextBody that already contains the decoded text
part.setBody(new TextBody(text));
return text;
} finally { } finally {
try { try {
/* MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in);
* 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();
} catch (IOException e) { /* Ignore */ } } catch (IOException e) { /* Ignore */ }
} }
} }
@ -186,6 +166,8 @@ public class MessageExtractor {
Html html = new Html(part); Html html = new Html(part);
viewables.add(html); viewables.add(html);
} }
} else if (part.getMimeType().equalsIgnoreCase("application/pgp-signature")) {
// ignore this type explicitly
} else { } else {
// Everything else is treated as attachment. // Everything else is treated as attachment.
attachments.add(part); 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.BodyPart;
import com.fsck.k9.mail.CompositeBody; import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
@ -73,7 +72,7 @@ public class MimeBodyPart extends BodyPart {
} }
@Override @Override
public void setBody(Body body) throws MessagingException { public void setBody(Body body) {
this.mBody = body; this.mBody = body;
} }
@ -86,7 +85,7 @@ public class MimeBodyPart extends BodyPart {
} }
@Override @Override
public String getContentType() throws MessagingException { public String getContentType() {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType; return (contentType == null) ? "text/plain" : contentType;
} }
@ -97,7 +96,7 @@ public class MimeBodyPart extends BodyPart {
} }
@Override @Override
public String getContentId() throws MessagingException { public String getContentId() {
String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID); String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
if (contentId == null) { if (contentId == null) {
return null; return null;
@ -112,7 +111,7 @@ public class MimeBodyPart extends BodyPart {
} }
@Override @Override
public String getMimeType() throws MessagingException { public String getMimeType() {
return MimeUtility.getHeaderParameter(getContentType(), null); 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 @Override
public void setUsing7bitTransport() throws MessagingException { public void setUsing7bitTransport() throws MessagingException {
String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);

View File

@ -11,28 +11,11 @@ import java.util.*;
public class MimeHeader { public class MimeHeader {
private static final String[] EMPTY_STRING_ARRAY = new String[0]; 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_TYPE = "Content-Type";
public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; 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_DISPOSITION = "Content-Disposition";
public static final String HEADER_CONTENT_ID = "Content-ID"; 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 List<Field> mFields = new ArrayList<Field>();
private String mCharset = null; private String mCharset = null;
@ -101,14 +84,12 @@ public class MimeHeader {
public void writeTo(OutputStream out) throws IOException { public void writeTo(OutputStream out) throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
for (Field field : mFields) { for (Field field : mFields) {
if (!Arrays.asList(writeOmitFields).contains(field.name)) { if (field.hasRawData()) {
if (field.hasRawData()) { writer.write(field.getRaw());
writer.write(field.getRaw()); } else {
} else { writeNameValueField(writer, field);
writeNameValueField(writer, field);
}
writer.write("\r\n");
} }
writer.write("\r\n");
} }
writer.flush(); writer.flush();
} }

View File

@ -57,6 +57,7 @@ public class MimeMessage extends Message {
private Body mBody; private Body mBody;
protected int mSize; protected int mSize;
private String serverExtra;
public MimeMessage() { public MimeMessage() {
} }
@ -162,7 +163,7 @@ public class MimeMessage extends Message {
} }
@Override @Override
public String getContentType() throws MessagingException { public String getContentType() {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType; return (contentType == null) ? "text/plain" : contentType;
} }
@ -171,12 +172,14 @@ public class MimeMessage extends Message {
public String getDisposition() throws MessagingException { public String getDisposition() throws MessagingException {
return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
} }
@Override @Override
public String getContentId() throws MessagingException { public String getContentId() {
return null; return null;
} }
@Override @Override
public String getMimeType() throws MessagingException { public String getMimeType() {
return MimeUtility.getHeaderParameter(getContentType(), null); return MimeUtility.getHeaderParameter(getContentType(), null);
} }
@ -308,13 +311,10 @@ public class MimeMessage extends Message {
if (mMessageId == null) { if (mMessageId == null) {
mMessageId = getFirstHeader("Message-ID"); mMessageId = getFirstHeader("Message-ID");
} }
if (mMessageId == null) { // even after checking the header
setMessageId(generateMessageId());
}
return mMessageId; return mMessageId;
} }
private String generateMessageId() { public void generateMessageId() throws MessagingException {
String hostname = null; String hostname = null;
if (mFrom != null && mFrom.length >= 1) { 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) */ /* 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 { public void setMessageId(String messageId) throws MessagingException {
@ -394,7 +396,7 @@ public class MimeMessage extends Message {
} }
@Override @Override
public void setBody(Body body) throws MessagingException { public void setBody(Body body) {
this.mBody = 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 @Override
public InputStream getInputStream() throws MessagingException { public InputStream getInputStream() throws MessagingException {
return null; return null;
@ -487,13 +494,11 @@ public class MimeMessage extends Message {
stack.addFirst(MimeMessage.this); stack.addFirst(MimeMessage.this);
} else { } else {
expect(Part.class); expect(Part.class);
try { Part part = (Part) stack.peek();
MimeMessage m = new MimeMessage();
((Part)stack.peek()).setBody(m); MimeMessage m = new MimeMessage();
stack.addFirst(m); part.setBody(m);
} catch (MessagingException me) { stack.addFirst(m);
throw new Error(me);
}
} }
} }
@ -519,7 +524,10 @@ public class MimeMessage extends Message {
Part e = (Part)stack.peek(); Part e = (Part)stack.peek();
try { 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); e.setBody(multiPart);
stack.addFirst(multiPart); stack.addFirst(multiPart);
} catch (MessagingException me) { } catch (MessagingException me) {
@ -540,7 +548,21 @@ public class MimeMessage extends Message {
@Override @Override
public void endMultipart() { 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 @Override
@ -686,4 +708,16 @@ public class MimeMessage extends Message {
setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE); 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) { if (body instanceof Multipart) {
Multipart multipart = ((Multipart) body); Multipart multipart = ((Multipart) body);
multipart.setParent(part); multipart.setParent(part);
String type = multipart.getContentType(); String mimeType = multipart.getMimeType();
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); String contentType = String.format("%s; boundary=\"%s\"", mimeType, multipart.getBoundary());
if ("multipart/signed".equalsIgnoreCase(type)) { part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
if ("multipart/signed".equalsIgnoreCase(mimeType)) {
setEncoding(part, MimeUtil.ENC_7BIT); setEncoding(part, MimeUtil.ENC_7BIT);
} else { } else {
setEncoding(part, MimeUtil.ENC_8BIT); setEncoding(part, MimeUtil.ENC_8BIT);

View File

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

View File

@ -16,10 +16,7 @@ import org.apache.james.mime4j.util.MimeUtil;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -1029,7 +1026,7 @@ public class MimeUtility {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
super.close(); super.close();
rawInputStream.close(); closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream);
} }
}; };
} else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) { } else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) {
@ -1037,7 +1034,7 @@ public class MimeUtility {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
super.close(); super.close();
rawInputStream.close(); closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream);
} }
}; };
} else { } else {
@ -1050,6 +1047,14 @@ public class MimeUtility {
return inputStream; 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) { public static String getMimeTypeByExtension(String filename) {
String returnedType = null; String returnedType = null;
String extension = 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.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import com.fsck.k9.mail.filter.CountingOutputStream;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil; import org.apache.james.mime4j.util.MimeUtil;
public class TextBody implements Body { public class TextBody implements Body, SizeAware {
/** /**
* Immutable empty byte array * Immutable empty byte array
@ -98,4 +99,33 @@ public class TextBody implements Body {
public void setComposedMessageOffset(Integer composedMessageOffset) { public void setComposedMessageOffset(Integer composedMessageOffset) {
this.mComposedMessageOffset = 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

@ -1,12 +1,14 @@
package com.fsck.k9.mail.store; package com.fsck.k9.mail.store;
import com.fsck.k9.mail.NetworkType;
public interface StoreConfig { public interface StoreConfig {
String getStoreUri(); String getStoreUri();
String getTransportUri(); String getTransportUri();
boolean subscribedFoldersOnly(); boolean subscribedFoldersOnly();
boolean useCompression(int type); boolean useCompression(NetworkType type);
String getInboxFolderName(); String getInboxFolderName();
String getOutboxFolderName(); String getOutboxFolderName();

View File

@ -11,6 +11,7 @@ import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.K9MailLib; import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.ssl.TrustedSocketFactory; import com.fsck.k9.mail.ssl.TrustedSocketFactory;
@ -471,7 +472,8 @@ class ImapConnection {
if (K9MailLib.isDebug()) { if (K9MailLib.isDebug()) {
Log.d(LOG_TAG, "On network type " + type); Log.d(LOG_TAG, "On network type " + type);
} }
useCompression = mSettings.useCompression(type); useCompression = mSettings.useCompression(
NetworkType.fromConnectivityManagerType(type));
} }
if (K9MailLib.isDebug()) { if (K9MailLib.isDebug()) {
Log.d(LOG_TAG, "useCompression " + useCompression); Log.d(LOG_TAG, "useCompression " + useCompression);

View File

@ -2,6 +2,7 @@ package com.fsck.k9.mail.store.imap;
import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.NetworkType;
/** /**
* Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link ImapConnection}. * Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link ImapConnection}.
@ -21,7 +22,7 @@ interface ImapSettings {
String getClientCertificateAlias(); String getClientCertificateAlias();
boolean useCompression(int type); boolean useCompression(NetworkType type);
String getPathPrefix(); String getPathPrefix();

View File

@ -37,6 +37,7 @@ import android.os.PowerManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMessageHelper;
import com.fsck.k9.mail.power.TracingPowerManager; import com.fsck.k9.mail.power.TracingPowerManager;
import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock;
@ -54,6 +55,7 @@ import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.ServerSettings.Type;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeBodyPart;
@ -145,13 +147,13 @@ public class ImapStore extends RemoteStore {
*/ */
if (scheme.equals("imap")) { if (scheme.equals("imap")) {
connectionSecurity = ConnectionSecurity.NONE; connectionSecurity = ConnectionSecurity.NONE;
port = 143; port = Type.IMAP.defaultPort;
} else if (scheme.startsWith("imap+tls")) { } else if (scheme.startsWith("imap+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 143; port = Type.IMAP.defaultPort;
} else if (scheme.startsWith("imap+ssl")) { } else if (scheme.startsWith("imap+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 993; port = Type.IMAP.defaultTlsPort;
} else { } else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
} }
@ -1454,13 +1456,9 @@ public class ImapStore extends RemoteStore {
throws MessagingException { throws MessagingException {
checkOpen(); //only need READ access checkOpen(); //only need READ access
String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); String partId = part.getServerExtra();
if (parts == null) {
return;
}
String fetch; String fetch;
String partId = parts[0];
if ("TEXT".equalsIgnoreCase(partId)) { if ("TEXT".equalsIgnoreCase(partId)) {
fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>", fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>",
mStoreConfig.getMaximumAutoDownloadMessageSize()); mStoreConfig.getMaximumAutoDownloadMessageSize());
@ -1713,7 +1711,7 @@ public class ImapStore extends RemoteStore {
break; break;
} }
} }
part.setBody(mp); MimeMessageHelper.setBody(part, mp);
} else { } else {
/* /*
* This is a body. We need to add as much information as we can find out about * This is a body. We need to add as much information as we can find out about
@ -1833,7 +1831,7 @@ public class ImapStore extends RemoteStore {
if (part instanceof ImapMessage) { if (part instanceof ImapMessage) {
((ImapMessage) part).setSize(size); ((ImapMessage) part).setSize(size);
} }
part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); part.setServerExtra(id);
} }
} }
@ -2932,7 +2930,7 @@ public class ImapStore extends RemoteStore {
} }
@Override @Override
public boolean useCompression(final int type) { public boolean useCompression(final NetworkType type) {
return mStoreConfig.useCompression(type); return mStoreConfig.useCompression(type);
} }

View File

@ -10,6 +10,7 @@ import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.ServerSettings.Type;
import com.fsck.k9.mail.ssl.TrustedSocketFactory; import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.RemoteStore; import com.fsck.k9.mail.store.RemoteStore;
import com.fsck.k9.mail.store.StoreConfig; import com.fsck.k9.mail.store.StoreConfig;
@ -100,13 +101,13 @@ public class Pop3Store extends RemoteStore {
*/ */
if (scheme.equals("pop3")) { if (scheme.equals("pop3")) {
connectionSecurity = ConnectionSecurity.NONE; connectionSecurity = ConnectionSecurity.NONE;
port = 110; port = Type.POP3.defaultPort;
} else if (scheme.startsWith("pop3+tls")) { } else if (scheme.startsWith("pop3+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 110; port = Type.POP3.defaultPort;
} else if (scheme.startsWith("pop3+ssl")) { } else if (scheme.startsWith("pop3+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 995; port = Type.POP3.defaultTlsPort;
} else { } else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
} }

View File

@ -75,13 +75,13 @@ public class SmtpTransport extends Transport {
*/ */
if (scheme.equals("smtp")) { if (scheme.equals("smtp")) {
connectionSecurity = ConnectionSecurity.NONE; connectionSecurity = ConnectionSecurity.NONE;
port = 587; port = ServerSettings.Type.SMTP.defaultPort;
} else if (scheme.startsWith("smtp+tls")) { } else if (scheme.startsWith("smtp+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED; connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 587; port = ServerSettings.Type.SMTP.defaultPort;
} else if (scheme.startsWith("smtp+ssl")) { } else if (scheme.startsWith("smtp+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED; connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 465; port = ServerSettings.Type.SMTP.defaultTlsPort;
} else { } else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")"); throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
} }

View File

@ -20,6 +20,7 @@ dependencies {
compile 'com.android.support:support-v13:21.0.2' compile 'com.android.support:support-v13:21.0.2'
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10' compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10'
compile 'de.cketti.library.changelog:ckchangelog:1.2.1' 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:testing-support-lib:0.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0' 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

@ -13,6 +13,8 @@ import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.NetworkType;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -167,7 +169,7 @@ public class ImapConnectionTest {
} }
@Override @Override
public boolean useCompression(int type) { public boolean useCompression(NetworkType type) {
return false; return false;
} }

View File

@ -1,6 +1,8 @@
package com.fsck.k9.mailstore; package com.fsck.k9.mailstore;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; 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.Address;
import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException; 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.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMessageHelper;
import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mail.internet.Viewable;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -38,7 +43,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, body); MimeMessageHelper.setBody(message, body);
// Extract text // 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 expectedText = bodyText;
String expectedHtml = String expectedHtml =
@ -63,7 +71,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, body); MimeMessageHelper.setBody(message, body);
// Extract text // 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 expectedText = "K-9 Mail rocks :>";
String expectedHtml = String expectedHtml =
@ -94,7 +105,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, multipart); MimeMessageHelper.setBody(message, multipart);
// Extract text // 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 = String expectedText =
bodyText1 + "\r\n\r\n" + bodyText1 + "\r\n\r\n" +
@ -151,7 +165,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, multipart); MimeMessageHelper.setBody(message, multipart);
// Extract text // 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 = String expectedText =
bodyText + 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:authorities="${applicationId}.provider.email"
android:exported="false"/> 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> </application>
</manifest> </manifest>

View File

@ -27,6 +27,7 @@ import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Folder.FolderClass; import com.fsck.k9.mail.Folder.FolderClass;
import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.Base64;
@ -96,12 +97,6 @@ public class Account implements BaseAccount, StoreConfig {
} }
} }
public enum NetworkType {
WIFI,
MOBILE,
OTHER
}
public static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML; public static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML;
public static final boolean DEFAULT_MESSAGE_FORMAT_AUTO = false; public static final boolean DEFAULT_MESSAGE_FORMAT_AUTO = false;
public static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false; public static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false;
@ -466,7 +461,8 @@ public class Account implements BaseAccount, StoreConfig {
mIsSignatureBeforeQuotedText = prefs.getBoolean(mUuid + ".signatureBeforeQuotedText", false); mIsSignatureBeforeQuotedText = prefs.getBoolean(mUuid + ".signatureBeforeQuotedText", false);
identities = loadIdentities(prefs); 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); mAllowRemoteSearch = prefs.getBoolean(mUuid + ".allowRemoteSearch", false);
mRemoteSearchFullText = prefs.getBoolean(mUuid + ".remoteSearchFullText", false); mRemoteSearchFullText = prefs.getBoolean(mUuid + ".remoteSearchFullText", false);
mRemoteSearchNumResults = prefs.getInt(mUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS); mRemoteSearchNumResults = prefs.getInt(mUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS);
@ -1303,19 +1299,6 @@ public class Account implements BaseAccount, StoreConfig {
return useCompression; return useCompression;
} }
public boolean useCompression(int type) {
NetworkType networkType = NetworkType.OTHER;
switch (type) {
case ConnectivityManager.TYPE_MOBILE:
networkType = NetworkType.MOBILE;
break;
case ConnectivityManager.TYPE_WIFI:
networkType = NetworkType.WIFI;
break;
}
return useCompression(networkType);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof Account) { if (o instanceof Account) {
@ -1615,7 +1598,11 @@ public class Account implements BaseAccount, StoreConfig {
} }
public void setCryptoApp(String cryptoApp) { public void setCryptoApp(String cryptoApp) {
mCryptoApp = cryptoApp; if (cryptoApp == null || cryptoApp.equals("apg")) {
mCryptoApp = NO_OPENPGP_PROVIDER;
} else {
mCryptoApp = cryptoApp;
}
} }
public boolean allowRemoteSearch() { public boolean allowRemoteSearch() {
@ -1659,13 +1646,16 @@ public class Account implements BaseAccount, StoreConfig {
} }
public synchronized String getOpenPgpProvider() { public synchronized String getOpenPgpProvider() {
// return null if set to "APG" or "None" if (!isOpenPgpProviderConfigured()) {
if (getCryptoApp().equals("apg") || getCryptoApp().equals("")) {
return null; return null;
} }
return getCryptoApp(); return getCryptoApp();
} }
public synchronized boolean isOpenPgpProviderConfigured() {
return !NO_OPENPGP_PROVIDER.equals(getCryptoApp());
}
public synchronized NotificationSetting getNotificationSetting() { public synchronized NotificationSetting getNotificationSetting() {
return mNotificationSetting; return mNotificationSetting;
} }

View File

@ -1,10 +1,10 @@
package com.fsck.k9.account; package com.fsck.k9.account;
import com.fsck.k9.Account.DeletePolicy; import com.fsck.k9.Account.DeletePolicy;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings.Type; import com.fsck.k9.mail.ServerSettings.Type;
import java.util.HashMap;
import java.util.Map;
/** /**
* Deals with logic surrounding account creation. * Deals with logic surrounding account creation.
@ -13,16 +13,36 @@ import java.util.Map;
*/ */
public class AccountCreator { public class AccountCreator {
private static Map<Type, DeletePolicy> defaults = new HashMap<Type, DeletePolicy>(); public static DeletePolicy getDefaultDeletePolicy(Type type) {
switch (type) {
case IMAP: {
return DeletePolicy.ON_DELETE;
}
case POP3: {
return DeletePolicy.NEVER;
}
case WebDAV: {
return DeletePolicy.ON_DELETE;
}
case SMTP: {
throw new IllegalStateException("Delete policy doesn't apply to SMTP");
}
}
static { throw new AssertionError("Unhandled case: " + type);
defaults.put(Type.IMAP, DeletePolicy.ON_DELETE);
defaults.put(Type.POP3, DeletePolicy.NEVER);
defaults.put(Type.WebDAV, DeletePolicy.ON_DELETE);
} }
public static DeletePolicy calculateDefaultDeletePolicy(Type type) { public static int getDefaultPort(ConnectionSecurity securityType, Type storeType) {
return defaults.get(type); switch (securityType) {
} case NONE:
case STARTTLS_REQUIRED: {
return storeType.defaultPort;
}
case SSL_TLS_REQUIRED: {
return storeType.defaultTlsPort;
}
}
throw new AssertionError("Unhandled ConnectionSecurity type encountered: " + securityType);
}
} }

View File

@ -1283,7 +1283,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
new String[] {"HtmlCleaner", "http://htmlcleaner.sourceforge.net/"}, new String[] {"HtmlCleaner", "http://htmlcleaner.sourceforge.net/"},
new String[] {"Android-PullToRefresh", "https://github.com/chrisbanes/Android-PullToRefresh"}, new String[] {"Android-PullToRefresh", "https://github.com/chrisbanes/Android-PullToRefresh"},
new String[] {"ckChangeLog", "https://github.com/cketti/ckChangeLog"}, 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() { private void onAbout() {

View File

@ -16,7 +16,6 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -43,7 +42,6 @@ import android.os.Parcelable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.util.Rfc822Tokenizer; import android.text.util.Rfc822Tokenizer;
import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
@ -89,7 +87,7 @@ import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.fragment.ProgressDialogFragment;
import com.fsck.k9.helper.ContactItem; import com.fsck.k9.helper.ContactItem;
import com.fsck.k9.helper.Contacts; 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.HtmlConverter;
import com.fsck.k9.helper.IdentityHelper; import com.fsck.k9.helper.IdentityHelper;
import com.fsck.k9.helper.Utility; 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.Multipart;
import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor; 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.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.MimeUtility;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.LocalAttachmentBody;
import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.TempFileBody; import com.fsck.k9.message.IdentityField;
import com.fsck.k9.mailstore.TempFileMessageBody; 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 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.CleanerProperties;
import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer; import org.htmlcleaner.SimpleHtmlSerializer;
@ -264,12 +260,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/ */
private Action mAction; private Action mAction;
private enum QuotedTextMode {
NONE,
SHOW,
HIDE
}
private boolean mReadReceipt = false; private boolean mReadReceipt = false;
private QuotedTextMode mQuotedTextMode = QuotedTextMode.NONE; private QuotedTextMode mQuotedTextMode = QuotedTextMode.NONE;
@ -298,20 +288,14 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private Button mQuotedTextShow; private Button mQuotedTextShow;
private View mQuotedTextBar; private View mQuotedTextBar;
private ImageButton mQuotedTextEdit; private ImageButton mQuotedTextEdit;
private ImageButton mQuotedTextDelete;
private EolConvertingEditText mQuotedText; private EolConvertingEditText mQuotedText;
private MessageWebView mQuotedHTML; private MessageWebView mQuotedHTML;
private InsertableHtmlContent mQuotedHtmlContent; // Container for HTML reply as it's being built. private InsertableHtmlContent mQuotedHtmlContent; // Container for HTML reply as it's being built.
private View mEncryptLayout;
private CheckBox mCryptoSignatureCheckbox; private CheckBox mCryptoSignatureCheckbox;
private CheckBox mEncryptCheckbox; private CheckBox mEncryptCheckbox;
private TextView mCryptoSignatureUserId; private TextView mCryptoSignatureUserId;
private TextView mCryptoSignatureUserIdRest; private TextView mCryptoSignatureUserIdRest;
private ImageButton mAddToFromContacts;
private ImageButton mAddCcFromContacts;
private ImageButton mAddBccFromContacts;
private PgpData mPgpData = null; private PgpData mPgpData = null;
private String mOpenPgpProvider; private String mOpenPgpProvider;
private OpenPgpServiceConnection mOpenPgpServiceConnection; private OpenPgpServiceConnection mOpenPgpServiceConnection;
@ -322,11 +306,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private boolean mSourceProcessed = false; private boolean mSourceProcessed = false;
enum SimpleMessageFormat {
TEXT,
HTML
}
/** /**
* The currently used message format. * The currently used message format.
* *
@ -410,8 +389,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}; };
private Listener mListener = new Listener(); private Listener mListener = new Listener();
private EmailAddressAdapter mAddressAdapter;
private Validator mAddressValidator;
private FontSizes mFontSizes = K9.getFontSizes(); private FontSizes mFontSizes = K9.getFontSizes();
private ContextThemeWrapper mThemeContext; 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 * Compose a new message as a reply to the given message. If replyAll is true the function
* is reply all instead of simply reply. * is reply all instead of simply reply.
* @param context * @param context
* @param account
* @param message * @param message
* @param replyAll * @param replyAll
* @param messageBody optional, for decrypted messages, null if it should be grabbed from the given message * @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) // theme the whole content according to the theme (except the action bar)
mThemeContext = new ContextThemeWrapper(this, mThemeContext = new ContextThemeWrapper(this,
K9.getK9ThemeResourceId(K9.getK9ComposerTheme())); K9.getK9ThemeResourceId(K9.getK9ComposerTheme()));
View v = ((LayoutInflater) mThemeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)). View v = LayoutInflater.from(mThemeContext).inflate(R.layout.message_compose, null);
inflate(R.layout.message_compose, null);
TypedValue outValue = new TypedValue(); TypedValue outValue = new TypedValue();
// background color needs to be forced // 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); v.setBackgroundColor(outValue.data);
setContentView(v); setContentView(v);
} else { } else {
@ -566,8 +541,8 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mContacts = Contacts.getInstance(MessageCompose.this); mContacts = Contacts.getInstance(MessageCompose.this);
mAddressAdapter = new EmailAddressAdapter(mThemeContext); EmailAddressAdapter mAddressAdapter = new EmailAddressAdapter(mThemeContext);
mAddressValidator = new EmailAddressValidator(); Validator mAddressValidator = new EmailAddressValidator();
mChooseIdentityButton = (Button) findViewById(R.id.identity); mChooseIdentityButton = (Button) findViewById(R.id.identity);
mChooseIdentityButton.setOnClickListener(this); mChooseIdentityButton.setOnClickListener(this);
@ -583,9 +558,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mSubjectView = (EditText) findViewById(R.id.subject); mSubjectView = (EditText) findViewById(R.id.subject);
mSubjectView.getInputExtras(true).putBoolean("allowEmoji", true); mSubjectView.getInputExtras(true).putBoolean("allowEmoji", true);
mAddToFromContacts = (ImageButton) findViewById(R.id.add_to); ImageButton mAddToFromContacts = (ImageButton) findViewById(R.id.add_to);
mAddCcFromContacts = (ImageButton) findViewById(R.id.add_cc); ImageButton mAddCcFromContacts = (ImageButton) findViewById(R.id.add_cc);
mAddBccFromContacts = (ImageButton) findViewById(R.id.add_bcc); ImageButton mAddBccFromContacts = (ImageButton) findViewById(R.id.add_bcc);
mCcWrapper = (LinearLayout) findViewById(R.id.cc_wrapper); mCcWrapper = (LinearLayout) findViewById(R.id.cc_wrapper);
mBccWrapper = (LinearLayout) findViewById(R.id.bcc_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); mQuotedTextShow = (Button)findViewById(R.id.quoted_text_show);
mQuotedTextBar = findViewById(R.id.quoted_text_bar); mQuotedTextBar = findViewById(R.id.quoted_text_bar);
mQuotedTextEdit = (ImageButton)findViewById(R.id.quoted_text_edit); 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 = (EolConvertingEditText)findViewById(R.id.quoted_text);
mQuotedText.getInputExtras(true).putBoolean("allowEmoji", true); mQuotedText.getInputExtras(true).putBoolean("allowEmoji", true);
@ -618,81 +594,34 @@ public class MessageCompose extends K9Activity implements OnClickListener,
} }
}); });
TextWatcher watcher = new TextWatcher() { TextWatcher draftNeedsChangingTextWatcher = new SimpleTextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int before, int after) {
/* do nothing */
}
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, int count) { public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true; 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 TextWatcher signTextWatcher = new SimpleTextWatcher() {
// 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 */
}
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, int count) { public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true; mDraftNeedsSaving = true;
mSignatureChanged = true; mSignatureChanged = true;
} }
@Override
public void afterTextChanged(android.text.Editable s) { /* do nothing */ }
}; };
mToView.addTextChangedListener(recipientWatcher); mToView.addTextChangedListener(draftNeedsChangingTextWatcher);
mCcView.addTextChangedListener(recipientWatcher); mCcView.addTextChangedListener(draftNeedsChangingTextWatcher);
mBccView.addTextChangedListener(recipientWatcher); mBccView.addTextChangedListener(draftNeedsChangingTextWatcher);
mSubjectView.addTextChangedListener(watcher); mSubjectView.addTextChangedListener(draftNeedsChangingTextWatcher);
mMessageContentView.addTextChangedListener(watcher); mMessageContentView.addTextChangedListener(draftNeedsChangingTextWatcher);
mQuotedText.addTextChangedListener(watcher); 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()) { if (mContacts.hasContactPicker()) {
mAddToFromContacts.setOnClickListener(new OnClickListener() { mAddToFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_TO));
@Override public void onClick(View v) { mAddCcFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_CC));
doLaunchContactPicker(CONTACT_PICKER_TO); mAddBccFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_BCC));
}
});
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);
}
});
} else { } else {
mAddToFromContacts.setVisibility(View.GONE); mAddToFromContacts.setVisibility(View.GONE);
mAddCcFromContacts.setVisibility(View.GONE); mAddCcFromContacts.setVisibility(View.GONE);
@ -762,7 +691,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mSignatureView = lowerSignature; mSignatureView = lowerSignature;
upperSignature.setVisibility(View.GONE); upperSignature.setVisibility(View.GONE);
} }
mSignatureView.addTextChangedListener(sigwatcher); mSignatureView.addTextChangedListener(signTextWatcher);
if (!mIdentity.getSignatureUse()) { if (!mIdentity.getSignatureUse()) {
mSignatureView.setVisibility(View.GONE); mSignatureView.setVisibility(View.GONE);
@ -814,34 +743,31 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mMessageReference = mMessageReference.withModifiedFlag(Flag.FORWARDED); mMessageReference = mMessageReference.withModifiedFlag(Flag.FORWARDED);
} }
mEncryptLayout = findViewById(R.id.layout_encrypt); final View 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);
}
initializeCrypto(); initializeCrypto();
mOpenPgpProvider = mAccount.getOpenPgpProvider(); 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 // New OpenPGP Provider API
// bind to service // 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. * Fill the encrypt layout with the latest data about signature key and encryption keys.
*/ */
public void updateEncryptLayout() { public void updateEncryptLayout() {
if (!isCryptoProviderEnabled()) {
return;
}
if (!mPgpData.hasSignatureKey()) { if (!mPgpData.hasSignatureKey()) {
mCryptoSignatureCheckbox.setText(R.string.btn_crypto_sign); mCryptoSignatureCheckbox.setText(R.string.btn_crypto_sign);
mCryptoSignatureCheckbox.setChecked(false); mCryptoSignatureCheckbox.setChecked(false);
@ -1093,16 +1023,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(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.putInt(STATE_KEY_NUM_ATTACHMENTS_LOADING, mNumAttachmentsLoading);
outState.putString(STATE_KEY_WAITING_FOR_ATTACHMENTS, mWaitingForAttachments.name()); 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_CC_SHOWN, mCcWrapper.getVisibility() == View.VISIBLE);
outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccWrapper.getVisibility() == View.VISIBLE); outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccWrapper.getVisibility() == View.VISIBLE);
outState.putSerializable(STATE_KEY_QUOTED_TEXT_MODE, mQuotedTextMode); outState.putSerializable(STATE_KEY_QUOTED_TEXT_MODE, mQuotedTextMode);
@ -1249,435 +1173,55 @@ public class MessageCompose extends K9Activity implements OnClickListener,
return Address.parseUnencoded(addresses.trim()); 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) { private TextBody buildText(boolean isDraft) {
return buildText(isDraft, mMessageFormat); return createMessageBuilder(isDraft).buildText();
} }
/** private MimeMessage createDraftMessage() throws MessagingException {
* Build the {@link Body} that will contain the text of the message. return createMessageBuilder(true).build();
*
* <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 createMessage() throws MessagingException {
* Add attachments as parts into a MimeMultipart container. return createMessageBuilder(false).build();
* @param mp MimeMultipart container in which to insert parts. }
* @throws MessagingException
*/ private MessageBuilder createMessageBuilder(boolean isDraft) {
private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException { return new MessageBuilder(getApplicationContext())
Body body; .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++) { for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); View view = mAttachments.getChildAt(i);
Attachment attachment = (Attachment) view.getTag();
if (attachment.state != Attachment.LoadingState.COMPLETE) { attachments.add(attachment);
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;
} }
public String value() { return attachments;
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;
} }
private void sendMessage() { private void sendMessage() {
@ -1690,7 +1234,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private void saveIfNeeded() { private void saveIfNeeded() {
if (!mDraftNeedsSaving || mPreventDraftSaving || mPgpData.hasEncryptionKeys() || if (!mDraftNeedsSaving || mPreventDraftSaving || mPgpData.hasEncryptionKeys() ||
mEncryptCheckbox.isChecked() || !mAccount.hasDraftsFolder()) { shouldEncrypt() || !mAccount.hasDraftsFolder()) {
return; return;
} }
@ -1734,7 +1278,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
} }
private void performSend() { private void performSend() {
if (mOpenPgpProvider != null) { if (isCryptoProviderEnabled()) {
// OpenPGP Provider API // OpenPGP Provider API
// If not already encrypted but user wants to encrypt... // 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()); Log.e(K9.LOG_TAG, "OpenPGP Error Message:" + error.getMessage());
Toast.makeText(MessageCompose.this, Toast.makeText(MessageCompose.this,
getString(R.string.openpgp_error) + " " + error.getMessage(), getString(R.string.openpgp_error, error.getMessage()),
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
} }
}); });
@ -1946,7 +1490,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/ */
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private void onAddAttachment2(final String mime_type) { 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(); Toast.makeText(this, R.string.attachment_encryption_unsupported, Toast.LENGTH_LONG).show();
} }
Intent i = new Intent(Intent.ACTION_GET_CONTENT); 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) { private void onAccountChosen(Account account, Identity identity) {
if (!mAccount.equals(account)) { if (!mAccount.equals(account)) {
if (K9.DEBUG) { if (K9.DEBUG) {
@ -2411,7 +1950,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
onSend(); onSend();
break; break;
case R.id.save: case R.id.save:
if (mEncryptCheckbox.isChecked()) { if (shouldEncrypt()) {
showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED); showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED);
} else { } else {
onSave(); onSave();
@ -2461,7 +2000,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (mDraftNeedsSaving) { if (mDraftNeedsSaving) {
if (mEncryptCheckbox.isChecked()) { if (shouldEncrypt()) {
showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED); showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED);
} else if (!mAccount.hasDraftsFolder()) { } else if (!mAccount.hasDraftsFolder()) {
showDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); showDialog(DIALOG_CONFIRM_DISCARD_ON_BACK);
@ -2622,17 +2161,19 @@ public class MessageCompose extends K9Activity implements OnClickListener,
String name = MimeUtility.getHeaderParameter(contentType, "name"); String name = MimeUtility.getHeaderParameter(contentType, "name");
if (name != null) { if (name != null) {
Body body = part.getBody(); Body body = part.getBody();
if (body instanceof LocalAttachmentBody) { //FIXME
final Uri uri = ((LocalAttachmentBody) body).getContentUri(); // if (body instanceof LocalAttachmentBody) {
mHandler.post(new Runnable() { // final Uri uri = ((LocalAttachmentBody) body).getContentUri();
@Override // mHandler.post(new Runnable() {
public void run() { // @Override
addAttachment(uri); // public void run() {
} // addAttachment(uri);
}); // }
} else { // });
return false; // } else {
} // return false;
// }
return false;
} }
return true; 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. // See buildIdentityHeader(TextBody) for a detailed description of the composition of this blob.
Map<IdentityField, String> k9identity = new HashMap<IdentityField, String>(); 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) { 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(); Identity newIdentity = new Identity();
@ -3400,8 +2941,8 @@ public class MessageCompose extends K9Activity implements OnClickListener,
} }
@Override @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.getUid().equals(uid)) { if ((mMessageReference == null) || !mMessageReference.uid.equals(uid)) {
return; return;
} }
@ -3554,7 +3095,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/ */
MimeMessage message; MimeMessage message;
try { try {
message = createMessage(false); // isDraft = true message = createMessage();
} catch (MessagingException me) { } catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", 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); 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; MimeMessage message;
try { try {
message = createMessage(true); // isDraft = true message = createDraftMessage();
} catch (MessagingException me) { } catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", 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); 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 // 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. // matter what the user selected for the message format.
messageFormat = SimpleMessageFormat.TEXT; 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 // Right now we only support PGP inline which doesn't play well with HTML. So force
// plain text in those cases. // plain text in those cases.
messageFormat = SimpleMessageFormat.TEXT; messageFormat = SimpleMessageFormat.TEXT;
@ -3952,34 +3493,30 @@ public class MessageCompose extends K9Activity implements OnClickListener,
} }
} }
/** private boolean isCryptoProviderEnabled() {
* An {@link EditText} extension with methods that convert line endings from return mOpenPgpProvider != null;
* {@code \r\n} to {@code \n} and back again when setting and getting text. }
*
*/
public static class EolConvertingEditText extends EditText {
public EolConvertingEditText(Context context, AttributeSet attrs) { private boolean shouldEncrypt() {
super(context, attrs); 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;
} }
/** @Override
* Return the text the EolConvertingEditText is displaying. public void onClick(View v) {
* mIgnoreOnPause = true;
* @return A string with any line endings converted to {@code \r\n}. startActivityForResult(mContacts.contactPickerIntent(), resultId);
*/
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

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

View File

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

View File

@ -321,7 +321,7 @@ public class AccountSetupBasics extends K9Activity
setupFolderNames(incomingUriTemplate.getHost().toLowerCase(Locale.US)); setupFolderNames(incomingUriTemplate.getHost().toLowerCase(Locale.US));
ServerSettings incomingSettings = RemoteStore.decodeStoreUri(incomingUri.toString()); ServerSettings incomingSettings = RemoteStore.decodeStoreUri(incomingUri.toString());
mAccount.setDeletePolicy(AccountCreator.calculateDefaultDeletePolicy(incomingSettings.type)); mAccount.setDeletePolicy(AccountCreator.getDefaultDeletePolicy(incomingSettings.type));
// Check incoming here. Then check outgoing in onActivityResult() // Check incoming here. Then check outgoing in onActivityResult()
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING);

View File

@ -17,7 +17,7 @@ import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.Account.FolderMode; import com.fsck.k9.Account.FolderMode;
import com.fsck.k9.Account.NetworkType; import com.fsck.k9.mail.NetworkType;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
@ -46,13 +46,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition"; private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition";
private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition"; private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition";
private static final String POP3_PORT = "110";
private static final String POP3_SSL_PORT = "995";
private static final String IMAP_PORT = "143";
private static final String IMAP_SSL_PORT = "993";
private static final String WEBDAV_PORT = "80";
private static final String WEBDAV_SSL_PORT = "443";
private Type mStoreType; private Type mStoreType;
private EditText mUsernameView; private EditText mUsernameView;
private EditText mPasswordView; private EditText mPasswordView;
@ -79,8 +72,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
private CheckBox mCompressionOther; private CheckBox mCompressionOther;
private CheckBox mSubscribedFoldersOnly; private CheckBox mSubscribedFoldersOnly;
private AuthTypeAdapter mAuthTypeAdapter; private AuthTypeAdapter mAuthTypeAdapter;
private String mDefaultPort = "";
private String mDefaultSslPort = "";
private ConnectionSecurity[] mConnectionSecurityChoices = ConnectionSecurity.values(); private ConnectionSecurity[] mConnectionSecurityChoices = ConnectionSecurity.values();
public static void actionIncomingSettings(Activity context, Account account, boolean makeDefault) { public static void actionIncomingSettings(Activity context, Account account, boolean makeDefault) {
@ -189,8 +180,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mStoreType = settings.type; mStoreType = settings.type;
if (Type.POP3 == settings.type) { if (Type.POP3 == settings.type) {
serverLabelView.setText(R.string.account_setup_incoming_pop_server_label); serverLabelView.setText(R.string.account_setup_incoming_pop_server_label);
mDefaultPort = POP3_PORT;
mDefaultSslPort = POP3_SSL_PORT;
findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE); findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE);
findViewById(R.id.webdav_advanced_header).setVisibility(View.GONE); findViewById(R.id.webdav_advanced_header).setVisibility(View.GONE);
findViewById(R.id.webdav_mailbox_alias_section).setVisibility(View.GONE); findViewById(R.id.webdav_mailbox_alias_section).setVisibility(View.GONE);
@ -201,8 +190,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mSubscribedFoldersOnly.setVisibility(View.GONE); mSubscribedFoldersOnly.setVisibility(View.GONE);
} else if (Type.IMAP == settings.type) { } else if (Type.IMAP == settings.type) {
serverLabelView.setText(R.string.account_setup_incoming_imap_server_label); serverLabelView.setText(R.string.account_setup_incoming_imap_server_label);
mDefaultPort = IMAP_PORT;
mDefaultSslPort = IMAP_SSL_PORT;
ImapStoreSettings imapSettings = (ImapStoreSettings) settings; ImapStoreSettings imapSettings = (ImapStoreSettings) settings;
@ -221,8 +208,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
} }
} else if (Type.WebDAV == settings.type) { } else if (Type.WebDAV == settings.type) {
serverLabelView.setText(R.string.account_setup_incoming_webdav_server_label); serverLabelView.setText(R.string.account_setup_incoming_webdav_server_label);
mDefaultPort = WEBDAV_PORT;
mDefaultSslPort = WEBDAV_SSL_PORT;
mConnectionSecurityChoices = new ConnectionSecurity[] { mConnectionSecurityChoices = new ConnectionSecurity[] {
ConnectionSecurity.NONE, ConnectionSecurity.NONE,
ConnectionSecurity.SSL_TLS_REQUIRED }; ConnectionSecurity.SSL_TLS_REQUIRED };
@ -252,7 +237,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
throw new Exception("Unknown account type: " + mAccount.getStoreUri()); throw new Exception("Unknown account type: " + mAccount.getStoreUri());
} }
mAccount.setDeletePolicy(AccountCreator.calculateDefaultDeletePolicy(settings.type)); mAccount.setDeletePolicy(AccountCreator.getDefaultDeletePolicy(settings.type));
// Note that mConnectionSecurityChoices is configured above based on server type // Note that mConnectionSecurityChoices is configured above based on server type
ConnectionSecurityAdapter securityTypesAdapter = ConnectionSecurityAdapter securityTypesAdapter =
@ -488,27 +473,10 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
// Remove listener so as not to trigger validateFields() which is called // Remove listener so as not to trigger validateFields() which is called
// elsewhere as a result of user interaction. // elsewhere as a result of user interaction.
mPortView.removeTextChangedListener(validationTextWatcher); mPortView.removeTextChangedListener(validationTextWatcher);
mPortView.setText(getDefaultPort(securityType)); mPortView.setText(String.valueOf(AccountCreator.getDefaultPort(securityType, mStoreType)));
mPortView.addTextChangedListener(validationTextWatcher); mPortView.addTextChangedListener(validationTextWatcher);
} }
private String getDefaultPort(ConnectionSecurity securityType) {
String port;
switch (securityType) {
case NONE:
case STARTTLS_REQUIRED:
port = mDefaultPort;
break;
case SSL_TLS_REQUIRED:
port = mDefaultSslPort;
break;
default:
Log.e(K9.LOG_TAG, "Unhandled ConnectionSecurity type encountered");
port = "";
}
return port;
}
private void updateAuthPlainTextFromSecurityType(ConnectionSecurity securityType) { private void updateAuthPlainTextFromSecurityType(ConnectionSecurity securityType) {
mAuthTypeAdapter.useInsecureText(securityType == ConnectionSecurity.NONE); mAuthTypeAdapter.useInsecureText(securityType == ConnectionSecurity.NONE);
} }

View File

@ -16,6 +16,7 @@ import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.account.AccountCreator;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
@ -38,9 +39,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition"; private static final String STATE_SECURITY_TYPE_POSITION = "stateSecurityTypePosition";
private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition"; private static final String STATE_AUTH_TYPE_POSITION = "authTypePosition";
private static final String SMTP_PORT = "587";
private static final String SMTP_SSL_PORT = "465";
private EditText mUsernameView; private EditText mUsernameView;
private EditText mPasswordView; private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner; private ClientCertificateSpinner mClientCertificateSpinner;
@ -427,27 +425,10 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
// Remove listener so as not to trigger validateFields() which is called // Remove listener so as not to trigger validateFields() which is called
// elsewhere as a result of user interaction. // elsewhere as a result of user interaction.
mPortView.removeTextChangedListener(validationTextWatcher); mPortView.removeTextChangedListener(validationTextWatcher);
mPortView.setText(getDefaultSmtpPort(securityType)); mPortView.setText(String.valueOf(AccountCreator.getDefaultPort(securityType, Type.SMTP)));
mPortView.addTextChangedListener(validationTextWatcher); mPortView.addTextChangedListener(validationTextWatcher);
} }
private String getDefaultSmtpPort(ConnectionSecurity securityType) {
String port;
switch (securityType) {
case NONE:
case STARTTLS_REQUIRED:
port = SMTP_PORT;
break;
case SSL_TLS_REQUIRED:
port = SMTP_SSL_PORT;
break;
default:
port = "";
Log.e(K9.LOG_TAG, "Unhandled ConnectionSecurity type encountered");
}
return port;
}
private void updateAuthPlainTextFromSecurityType(ConnectionSecurity securityType) { private void updateAuthPlainTextFromSecurityType(ConnectionSecurity securityType) {
mAuthTypeAdapter.useInsecureText(securityType == ConnectionSecurity.NONE); mAuthTypeAdapter.useInsecureText(securityType == ConnectionSecurity.NONE);
} }

View File

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

View File

@ -91,7 +91,7 @@ public class MessagingListener {
Message message) {} Message message) {}
public void loadMessageForViewFinished(Account account, String folder, String uid, public void loadMessageForViewFinished(Account account, String folder, String uid,
Message message) {} LocalMessage message) {}
public void loadMessageForViewFailed(Account account, String folder, String uid, public void loadMessageForViewFailed(Account account, String folder, String uid,
Throwable t) {} Throwable t) {}
@ -133,13 +133,9 @@ public class MessagingListener {
public void setPushActive(Account account, String folderName, boolean enabled) {} public void setPushActive(Account account, String folderName, boolean enabled) {}
public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, public void loadAttachmentFinished(Account account, Message message, Part part) {}
boolean requiresDownload) {}
public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) {} public void loadAttachmentFailed(Account account, Message message, Part part, String reason) {}
public void loadAttachmentFailed(Account account, Message message, Part part, Object tag,
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) { public static boolean hasExternalImages(final String message) {
Matcher imgMatches = IMG_PATTERN.matcher(message); Matcher imgMatches = IMG_PATTERN.matcher(message);
while (imgMatches.find()) { while (imgMatches.find()) {
if (!imgMatches.group(1).equals("content")) { String uriScheme = imgMatches.group(1);
if (uriScheme.equals("http") || uriScheme.equals("https")) {
if (K9.DEBUG) { if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "External images found"); 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.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Set; import java.util.Set;
import android.content.ContentValues; import android.content.ContentValues;
@ -37,10 +35,11 @@ public class LocalMessage extends MimeMessage {
private String mPreview = ""; private String mPreview = "";
private boolean mHeadersLoaded = false; private boolean mHeadersLoaded = false;
private boolean mMessageDirty = false;
private long mThreadId; private long mThreadId;
private long mRootId; private long mRootId;
private long messagePartId;
private String mimeType;
private LocalMessage(LocalStore localStore) { private LocalMessage(LocalStore localStore) {
this.localStore = localStore; this.localStore = localStore;
@ -111,30 +110,19 @@ public class LocalMessage extends MimeMessage {
setFlagInternal(Flag.FLAGGED, flagged); setFlagInternal(Flag.FLAGGED, flagged);
setFlagInternal(Flag.ANSWERED, answered); setFlagInternal(Flag.ANSWERED, answered);
setFlagInternal(Flag.FORWARDED, forwarded); setFlagInternal(Flag.FORWARDED, forwarded);
messagePartId = cursor.getLong(22);
mimeType = cursor.getString(23);
} }
/** long getMessagePartId() {
* Fetch the message text for display. This always returns an HTML-ified version of the return messagePartId;
* 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;
} }
@Override
public String getMimeType() {
return mimeType;
}
/* Custom version of writeTo that updates the MIME message based on localMessage /* Custom version of writeTo that updates the MIME message based on localMessage
* changes. * changes.
@ -142,30 +130,13 @@ public class LocalMessage extends MimeMessage {
@Override @Override
public void writeTo(OutputStream out) throws IOException, MessagingException { public void writeTo(OutputStream out) throws IOException, MessagingException {
if (mMessageDirty) buildMimeRepresentation(); if (!mHeadersLoaded) {
loadHeaders();
}
super.writeTo(out); 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 @Override
public String getPreview() { public String getPreview() {
return mPreview; return mPreview;
@ -180,14 +151,12 @@ public class LocalMessage extends MimeMessage {
@Override @Override
public void setSubject(String subject) throws MessagingException { public void setSubject(String subject) throws MessagingException {
mSubject = subject; mSubject = subject;
mMessageDirty = true;
} }
@Override @Override
public void setMessageId(String messageId) { public void setMessageId(String messageId) {
mMessageId = messageId; mMessageId = messageId;
mMessageDirty = true;
} }
@Override @Override
@ -208,7 +177,6 @@ public class LocalMessage extends MimeMessage {
@Override @Override
public void setFrom(Address from) throws MessagingException { public void setFrom(Address from) throws MessagingException {
this.mFrom = new Address[] { from }; this.mFrom = new Address[] { from };
mMessageDirty = true;
} }
@ -219,7 +187,6 @@ public class LocalMessage extends MimeMessage {
} else { } else {
mReplyTo = replyTo; mReplyTo = replyTo;
} }
mMessageDirty = true;
} }
@ -250,7 +217,6 @@ public class LocalMessage extends MimeMessage {
} else { } else {
throw new MessagingException("Unrecognized recipient type."); throw new MessagingException("Unrecognized recipient type.");
} }
mMessageDirty = true;
} }
public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 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 * If a message is being marked as deleted we want to clear out its content. Delete will not actually remove the
* and attachments as well. Delete will not actually remove the row since we need * row since we need to retain the UID for synchronization purposes.
* to retain the uid for synchronization purposes.
*/ */
private void delete() throws MessagingException private void delete() throws MessagingException {
{
/*
* Delete all of the message's content to save space.
*/
try { try {
this.localStore.database.execute(true, new DbCallback<Void>() { localStore.database.execute(true, new DbCallback<Void>() {
@Override @Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
UnavailableStorageException {
String[] idArg = new String[] { Long.toString(mId) };
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put("deleted", 1); cv.put("deleted", 1);
cv.put("empty", 1); cv.put("empty", 1);
@ -328,33 +285,25 @@ public class LocalMessage extends MimeMessage {
cv.putNull("cc_list"); cv.putNull("cc_list");
cv.putNull("bcc_list"); cv.putNull("bcc_list");
cv.putNull("preview"); cv.putNull("preview");
cv.putNull("html_content");
cv.putNull("text_content");
cv.putNull("reply_to_list"); 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 { try {
((LocalFolder) mFolder).deleteAttachments(mId); ((LocalFolder) mFolder).deleteMessagePartsAndDataFromDisk(messagePartId);
} catch (MessagingException e) { } catch (MessagingException e) {
throw new WrappedException(e); throw new WrappedException(e);
} }
db.delete("attachments", "message_id = ?", idArg);
return null; return null;
} }
}); });
} catch (WrappedException e) { } 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 { try {
LocalFolder localFolder = (LocalFolder) mFolder; LocalFolder localFolder = (LocalFolder) mFolder;
localFolder.deleteAttachments(mId); localFolder.deleteMessagePartsAndDataFromDisk(messagePartId);
if (hasThreadChildren(db, mId)) { if (hasThreadChildren(db, mId)) {
// This message has children in the thread structure so we need to // 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 { private void loadHeaders() throws MessagingException {
List<LocalMessage> messages = new ArrayList<LocalMessage>(); mHeadersLoaded = true;
messages.add(this); getFolder().populateHeaders(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");
} }
@Override @Override
@ -557,7 +491,6 @@ public class LocalMessage extends MimeMessage {
message.mSubject = mSubject; message.mSubject = mSubject;
message.mPreview = mPreview; message.mPreview = mPreview;
message.mHeadersLoaded = mHeadersLoaded; message.mHeadersLoaded = mHeadersLoaded;
message.mMessageDirty = mMessageDirty;
return message; return message;
} }
@ -618,4 +551,8 @@ public class LocalMessage extends MimeMessage {
private String getAccountUuid() { private String getAccountUuid() {
return getAccount().getUuid(); return getAccount().getUuid();
} }
public boolean isBodyMissing() {
return getBody() == null;
}
} }

View File

@ -1,19 +1,28 @@
package com.fsck.k9.mailstore; package com.fsck.k9.mailstore;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import com.fsck.k9.R; import com.fsck.k9.R;
import com.fsck.k9.crypto.DecryptedTempFileBody;
import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part; import com.fsck.k9.mail.Part;
import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mail.internet.MessageExtractor; 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.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.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; 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.Text;
import static com.fsck.k9.mail.internet.Viewable.Textual; import static com.fsck.k9.mail.internet.Viewable.Textual;
class LocalMessageExtractor { public class LocalMessageExtractor {
private static final String TEXT_DIVIDER = private static final String TEXT_DIVIDER =
"------------------------------------------------------------------------"; "------------------------------------------------------------------------";
private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); 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 int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length();
private static final String FILENAME_SUFFIX = " "; private static final String FILENAME_SUFFIX = " ";
private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length();
private static final OpenPgpResultAnnotation NO_ANNOTATIONS = null;
private LocalMessageExtractor() {} private LocalMessageExtractor() {}
/** /**
* Extract the viewable textual parts of a message and return the rest as attachments. * 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 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 * @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. * plain text and HTML, and a list of message parts considered attachments.
* *
* @throws com.fsck.k9.mail.MessagingException * @throws com.fsck.k9.mail.MessagingException
* In case of an error. * 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 { try {
List<Part> attachments = new ArrayList<Part>();
// Collect all viewable parts // Collect all viewable parts
List<Viewable> viewables = MessageExtractor.getViewables(message, attachments);
/* /*
* Convert the tree of viewable parts into text and HTML * 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. * 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>"); html.append("</td></tr>");
} }
private static ViewableContainer extractTextual(Part part) throws MessagingException { public static MessageViewInfo decodeMessageForView(Context context,
String text = ""; Message message, MessageCryptoAnnotations annotations) throws MessagingException {
String html = "";
List<Part> attachments = new ArrayList<Part>();
Body firstBody = part.getBody(); // 1. break mime structure on encryption/signature boundaries
if (part.isMimeType("text/plain")) { List<Part> parts = getCryptPieces(message, annotations);
String bodyText = MessageExtractor.getTextFromPart(part);
if (bodyText != null) { // 2. extract viewables/attachments of parts
text = bodyText; ArrayList<MessageViewContainer> containers = new ArrayList<MessageViewContainer>();
html = HtmlConverter.textToHtml(text); 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) { ArrayList<Part> attachments = new ArrayList<Part>();
MimeMultipart multipart = (MimeMultipart) firstBody; List<Viewable> viewables = MessageExtractor.getViewables(part, attachments);
for (BodyPart bodyPart : multipart.getBodyParts()) {
String bodyText = MessageExtractor.getTextFromPart(bodyPart); // 3. parse viewables into html string
if (bodyText != null) { ViewableContainer viewable = LocalMessageExtractor.extractTextAndAttachments(context, viewables,
if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { attachments);
text = bodyText; List<AttachmentViewInfo> attachmentInfos = extractAttachmentInfos(context, attachments);
} else if (html.isEmpty() && bodyPart.isMimeType("text/html")) {
html = bodyText; 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.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store; 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.StorageManager.StorageProvider;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.WrappedException; 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.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchField; import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.search.SqlQueryBuilder; 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; 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, " + "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " +
"bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " + "bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " +
"folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " + "folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " +
"forwarded "; "forwarded, message_part_id, mime_type ";
static final String GET_FOLDER_COLS = static final String GET_FOLDER_COLS =
"folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " + "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; 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) { public static String getColumnNameForFlag(Flag flag) {
@ -275,7 +284,7 @@ public class LocalStore extends Store implements Serializable {
if (K9.DEBUG) if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Before prune size = " + getSize()); Log.i(K9.LOG_TAG, "Before prune size = " + getSize());
pruneCachedAttachments(true); deleteAllMessageDataFromDisk();
if (K9.DEBUG) { if (K9.DEBUG) {
Log.i(K9.LOG_TAG, "After prune / before compaction size = " + getSize()); 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()); 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>() { database.execute(false, new DbCallback<Void>() {
@Override @Override
public Void doDbWork(final SQLiteDatabase db) { public Void doDbWork(final SQLiteDatabase db) {
// Delete entries from 'threads' table // We don't care about threads of deleted messages, so delete the whole table.
db.execSQL("DELETE FROM threads WHERE message_id IN " + db.delete("threads", null, null);
"(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')");
// Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make // Don't delete deleted messages. They are essentially placeholders for UIDs of messages that have
// sure the thread structure is in a valid state (this may destroy existing valid // been deleted locally.
// thread trees, but is much faster than adjusting the tree by removing messages db.delete("messages", "deleted = 0", null);
// 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%'");
return null; return null;
} }
}); });
@ -405,73 +406,39 @@ public class LocalStore extends Store implements Serializable {
database.recreate(); database.recreate();
} }
/** private void deleteAllMessageDataFromDisk() throws MessagingException {
* Deletes all cached attachments for the entire store. markAllMessagePartsDataAsMissing();
* @param force deleteAllMessagePartsDataFromDisk();
* @throws com.fsck.k9.mail.MessagingException }
*/
//TODO this method seems to be only called with force=true, simplify accordingly private void markAllMessagePartsDataAsMissing() throws MessagingException {
private void pruneCachedAttachments(final boolean force) throws MessagingException {
database.execute(false, new DbCallback<Void>() { database.execute(false, new DbCallback<Void>() {
@Override @Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException { public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
if (force) { ContentValues cv = new ContentValues();
ContentValues cv = new ContentValues(); cv.put("data_location", DataLocation.MISSING);
cv.putNull("content_uri"); db.update("message_parts", cv, null, null);
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();
}
}
}
return 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 { public void resetVisibleLimits(int visibleLimit) throws MessagingException {
final ContentValues cv = new ContentValues(); final ContentValues cv = new ContentValues();
cv.put("visible_limit", Integer.toString(visibleLimit)); 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>() { return database.execute(false, new DbCallback<AttachmentInfo>() {
@Override @Override
public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException { public AttachmentInfo doDbWork(final SQLiteDatabase db) throws WrappedException {
String name; Cursor cursor = db.query("message_parts",
String type; new String[] { "display_name", "decoded_body_size", "mime_type" },
int size; "id = ?",
Cursor cursor = null; new String[] { attachmentId },
null, null, null);
try { try {
cursor = db.query(
"attachments",
new String[] { "name", "size", "mime_type" },
"id = ?",
new String[] { attachmentId },
null,
null,
null);
if (!cursor.moveToFirst()) { if (!cursor.moveToFirst()) {
return null; return null;
} }
name = cursor.getString(0); String name = cursor.getString(0);
size = cursor.getInt(1); long size = cursor.getLong(1);
type = cursor.getString(2); String mimeType = cursor.getString(2);
final AttachmentInfo attachmentInfo = new AttachmentInfo(); final AttachmentInfo attachmentInfo = new AttachmentInfo();
attachmentInfo.name = name; attachmentInfo.name = name;
attachmentInfo.size = size; attachmentInfo.size = size;
attachmentInfo.type = type; attachmentInfo.type = mimeType;
return attachmentInfo; return attachmentInfo;
} finally { } 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 static class AttachmentInfo {
public String name; public String name;
public int size; public long size;
public String type; 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", Log.i(K9.LOG_TAG, String.format(Locale.US, "Upgrading database from version %d to version %d",
db.getVersion(), LocalStore.DB_VERSION)); db.getVersion(), LocalStore.DB_VERSION));
AttachmentProvider.clear(this.localStore.context);
db.beginTransaction(); db.beginTransaction();
try { try {
// schema version 29 was when we moved to incremental updates // schema version 29 was when we moved to incremental updates
@ -83,8 +81,6 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
"cc_list TEXT, " + "cc_list TEXT, " +
"bcc_list TEXT, " + "bcc_list TEXT, " +
"reply_to_list TEXT, " + "reply_to_list TEXT, " +
"html_content TEXT, " +
"text_content TEXT, " +
"attachment_count INTEGER, " + "attachment_count INTEGER, " +
"internal_date INTEGER, " + "internal_date INTEGER, " +
"message_id TEXT, " + "message_id TEXT, " +
@ -95,12 +91,36 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
"read INTEGER default 0, " + "read INTEGER default 0, " +
"flagged INTEGER default 0, " + "flagged INTEGER default 0, " +
"answered 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 message_parts (" +
db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)"); "id INTEGER PRIMARY KEY, " +
db.execSQL("CREATE INDEX IF NOT EXISTS header_folder ON headers (message_id)"); "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("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)");
db.execSQL("DROP INDEX IF EXISTS msg_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; " + "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " +
"END"); "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("DROP TABLE IF EXISTS pending_commands");
db.execSQL("CREATE TABLE pending_commands " + db.execSQL("CREATE TABLE pending_commands " +
"(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)"); "(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("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("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; " db.execSQL("CREATE TRIGGER delete_message " +
+ "DELETE FROM headers where old.id = message_id; END;"); "BEFORE DELETE ON messages " +
"BEGIN " +
"DELETE FROM message_parts WHERE root = OLD.message_part_id;" +
"END");
} else { } else {
// in the case that we're starting out at 29 or newer, run all the needed updates // 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 = ?", db.update("folders", cv, "name = ?",
new String[] { this.localStore.getAccount().getInboxFolderName() }); new String[] { this.localStore.getAccount().getInboxFolderName() });
} }
if (db.getVersion() < 51) {
throw new IllegalStateException("Database upgrade not supported yet!");
}
} }
db.setVersion(LocalStore.DB_VERSION); db.setVersion(LocalStore.DB_VERSION);

View File

@ -7,11 +7,13 @@ import java.io.FileNotFoundException;
import java.io.InputStream; import java.io.InputStream;
import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.SizeAware;
/** /**
* An attachment whose contents are contained in a file. * 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; private final File mFile;
public TempFileBody(String filename) { public TempFileBody(String filename) {
@ -26,4 +28,9 @@ public class TempFileBody extends BinaryAttachmentBody {
return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY); 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; import com.fsck.k9.mail.MessagingException;
/** /**
* An attachment containing a body of type message/rfc822 * An attachment containing a body of type message/rfc822 whose contents are contained in a file.
* whose contents are contained in a file.
*/ */
public class TempFileMessageBody extends TempFileBody implements CompositeBody { public class TempFileMessageBody extends TempFileBody implements CompositeBody {
@ -25,7 +24,12 @@ public class TempFileMessageBody extends TempFileBody implements CompositeBody {
@Override @Override
public void setUsing7bitTransport() throws MessagingException { 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 @Override

View File

@ -10,7 +10,7 @@ import java.util.List;
* *
* @see LocalMessageExtractor#extractTextAndAttachments(android.content.Context, com.fsck.k9.mail.Message) * @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. * 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; 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? * 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; private static final long serialVersionUID = 2397327034L;
// Default to a headerInsertionPoint at the beginning of the message. // Default to a headerInsertionPoint at the beginning of the message.
private int headerInsertionPoint = 0; 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.text.TextUtils;
import android.util.Log; import android.util.Log;

View File

@ -1,15 +1,19 @@
package com.fsck.k9.provider; 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.ContentProvider;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.util.Log; import android.util.Log;
import com.fsck.k9.Account; import com.fsck.k9.Account;
import com.fsck.k9.BuildConfig; import com.fsck.k9.BuildConfig;
import com.fsck.k9.K9; 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.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStore;
import com.fsck.k9.mailstore.LocalStore.AttachmentInfo; 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. * 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 { public class AttachmentProvider extends ContentProvider {
private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".attachmentprovider"; private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".attachmentprovider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); 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[] { private static final String[] DEFAULT_PROJECTION = new String[] {
AttachmentProviderColumns._ID, AttachmentProviderColumns._ID,
AttachmentProviderColumns.DATA, AttachmentProviderColumns.DATA,
}; };
public static class AttachmentProviderColumns { 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() return CONTENT_URI.buildUpon()
.appendPath(account.getUuid()) .appendPath(accountUuid)
.appendPath(Long.toString(id)) .appendPath(Long.toString(id))
.appendPath(FORMAT_RAW)
.build(); .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 @Override
public boolean onCreate() { 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; return true;
} }
@Override @Override
public String getType(Uri uri) { public String getType(Uri uri) {
List<String> segments = uri.getPathSegments(); List<String> segments = uri.getPathSegments();
String dbName = segments.get(0); String accountUuid = segments.get(0);
String id = segments.get(1); String id = segments.get(1);
String format = segments.get(2); String mimeType = (segments.size() < 3) ? null : segments.get(2);
String mimeType = (segments.size() < 4) ? null : segments.get(3);
return getType(dbName, id, format, mimeType); return getType(accountUuid, id, mimeType);
} }
@Override @Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
File file;
List<String> segments = uri.getPathSegments(); List<String> segments = uri.getPathSegments();
String accountUuid = segments.get(0); String accountUuid = segments.get(0);
String attachmentId = segments.get(1); String attachmentId = segments.get(1);
String format = segments.get(2);
if (FORMAT_THUMBNAIL.equals(format)) { return openAttachment(accountUuid, attachmentId);
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);
} }
@Override @Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String sortOrder) {
String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection; String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection;
List<String> segments = uri.getPathSegments(); List<String> segments = uri.getPathSegments();
String dbName = segments.get(0); String accountUuid = segments.get(0);
String id = segments.get(1); 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; final AttachmentInfo attachmentInfo;
try { try {
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName); final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid);
attachmentInfo = LocalStore.getInstance(account, getContext()).getAttachmentInfo(id); attachmentInfo = LocalStore.getInstance(account, getContext()).getAttachmentInfo(id);
} catch (MessagingException e) { } catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Unable to retrieve attachment info from local store for ID: " + id, 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; return null;
} }
private String getType(String dbName, String id, String format, String mimeType) { private String getType(String accountUuid, String id, String mimeType) {
String type; String type;
if (FORMAT_THUMBNAIL.equals(format)) { final Account account = Preferences.getPreferences(getContext()).getAccount(accountUuid);
type = "image/png";
} else {
final Account account = Preferences.getPreferences(getContext()).getAccount(dbName);
try { try {
final LocalStore localStore = LocalStore.getInstance(account, getContext()); final LocalStore localStore = LocalStore.getInstance(account, getContext());
AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id); AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id);
if (FORMAT_VIEW.equals(format) && mimeType != null) { if (mimeType != null) {
type = mimeType; type = mimeType;
} else { } else {
type = attachmentInfo.type; type = attachmentInfo.type;
}
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e);
type = null;
} }
} catch (MessagingException e) {
Log.e(K9.LOG_TAG, "Unable to retrieve LocalStore for " + account, e);
type = MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE;
} }
return type; return type;
} }
private File getFile(String dbName, String id) throws FileNotFoundException { private ParcelFileDescriptor openAttachment(String accountUuid, String attachmentId) {
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) {
try { try {
Bitmap bitmap = BitmapFactory.decodeStream(data); InputStream inputStream = getAttachmentInputStream(accountUuid, attachmentId);
return bitmap; return ParcelFileDescriptorUtil.pipeFrom(inputStream, null);
} catch (OutOfMemoryError oome) { } catch (MessagingException e) {
/* Log.e(K9.LOG_TAG, "Error getting InputStream for attachment", e);
* 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.
*/
return null; return null;
} catch (Exception e) { } catch (IOException e) {
Log.e(K9.LOG_TAG, "Error creating ParcelFileDescriptor", e);
return null; 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 { private interface InternalMessageColumns extends MessageColumns {
public static final String DELETED = "deleted"; public static final String DELETED = "deleted";
public static final String EMPTY = "empty"; 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"; 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; break;
} }
case MESSAGE_CONTENTS: { case MESSAGE_CONTENTS: {
columnName = "text_content"; throw new RuntimeException("Searching in message bodies is currently not supported");
break;
} }
case REPLY_TO: { case REPLY_TO: {
columnName = "reply_to_list"; 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.getFolderName(), messageReference.getUid());
}
}

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);
}
}

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