Switched to Apache commons compress

pull/207/head
M66B 4 years ago
parent 9cbc99018e
commit 19d59a2203

@ -41,3 +41,4 @@ FairEmail uses:
* [Cousine font](https://fonts.google.com/specimen/Cousine). By Steve Matteson. [Apache License 2.0](https://fonts.google.com/specimen/Cousine#license). * [Cousine font](https://fonts.google.com/specimen/Cousine). By Steve Matteson. [Apache License 2.0](https://fonts.google.com/specimen/Cousine#license).
* [Lato font](https://fonts.google.com/specimen/Lato). By Łukasz Dziedzic. [Apache License 2.0](https://fonts.google.com/specimen/Lato#license). * [Lato font](https://fonts.google.com/specimen/Lato). By Łukasz Dziedzic. [Apache License 2.0](https://fonts.google.com/specimen/Lato#license).
* [Caladea font](https://fonts.google.com/specimen/Caladea). By Andrés Torresi, Carolina Giovanolli. [Apache License 2.0](https://fonts.google.com/specimen/Caladea#license). * [Caladea font](https://fonts.google.com/specimen/Caladea). By Andrés Torresi, Carolina Giovanolli. [Apache License 2.0](https://fonts.google.com/specimen/Caladea#license).
* [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/). Copyright © 2002-2021 The Apache Software Foundation. All Rights Reserved. [Apache License 2.0](https://www.apache.org/licenses/).

@ -376,6 +376,7 @@ dependencies {
def reactivestreams_version = "1.0.3" def reactivestreams_version = "1.0.3"
def rxjava2_version = "2.2.21" def rxjava2_version = "2.2.21"
def svg_version = "1.4" def svg_version = "1.4"
def compress_version = "1.21"
// https://developer.android.com/jetpack/androidx/releases/startup // https://developer.android.com/jetpack/androidx/releases/startup
implementation "androidx.startup:startup-runtime:$startup_version" implementation "androidx.startup:startup-runtime:$startup_version"
@ -572,4 +573,8 @@ dependencies {
// http://bigbadaboom.github.io/androidsvg/ // http://bigbadaboom.github.io/androidsvg/
implementation "com.caverock:androidsvg:$svg_version" implementation "com.caverock:androidsvg:$svg_version"
// https://commons.apache.org/proper/commons-compress/
// https://mvnrepository.com/artifact/org.apache.commons/commons-compress
implementation "org.apache.commons:commons-compress:$compress_version"
} }

@ -41,3 +41,4 @@ FairEmail uses:
* [Cousine font](https://fonts.google.com/specimen/Cousine). By Steve Matteson. [Apache License 2.0](https://fonts.google.com/specimen/Cousine#license). * [Cousine font](https://fonts.google.com/specimen/Cousine). By Steve Matteson. [Apache License 2.0](https://fonts.google.com/specimen/Cousine#license).
* [Lato font](https://fonts.google.com/specimen/Lato). By Łukasz Dziedzic. [Apache License 2.0](https://fonts.google.com/specimen/Lato#license). * [Lato font](https://fonts.google.com/specimen/Lato). By Łukasz Dziedzic. [Apache License 2.0](https://fonts.google.com/specimen/Lato#license).
* [Caladea font](https://fonts.google.com/specimen/Caladea). By Andrés Torresi, Carolina Giovanolli. [Apache License 2.0](https://fonts.google.com/specimen/Caladea#license). * [Caladea font](https://fonts.google.com/specimen/Caladea). By Andrés Torresi, Carolina Giovanolli. [Apache License 2.0](https://fonts.google.com/specimen/Caladea#license).
* [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/). Copyright © 2002-2021 The Apache Software Foundation. All Rights Reserved. [Apache License 2.0](https://www.apache.org/licenses/).

@ -100,6 +100,39 @@ public class EntityAttachment {
return ImageHelper.isImage(getMimeType()); return ImageHelper.isImage(getMimeType());
} }
boolean isCompressed() {
if ("application/zip".equals(type))
return true;
if ("application/gzip".equals(type) && !BuildConfig.PLAY_STORE_RELEASE)
return true;
String extension = Helper.getExtension(name);
if ("zip".equals(extension))
return true;
if ("gz".equals(extension) && !BuildConfig.PLAY_STORE_RELEASE)
return true;
return false;
}
boolean isGzip() {
if (BuildConfig.PLAY_STORE_RELEASE)
return false;
if ("application/gzip".equals(type))
return true;
String extension = Helper.getExtension(name);
if ("gz".equals(extension))
return true;
return false;
}
boolean isTarGzip() {
return (name != null && name.endsWith(".tar.gz"));
}
boolean isEncryption() { boolean isEncryption() {
if ("application/pkcs7-mime".equals(type)) if ("application/pkcs7-mime".equals(type))
return true; return true;

@ -167,6 +167,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
private SwitchCompat swImagesInline; private SwitchCompat swImagesInline;
private SwitchCompat swButtonExtra; private SwitchCompat swButtonExtra;
private SwitchCompat swUnzip; private SwitchCompat swUnzip;
private TextView tvUnzipHint;
private SwitchCompat swAttachmentsAlt; private SwitchCompat swAttachmentsAlt;
private SwitchCompat swThumbnails; private SwitchCompat swThumbnails;
@ -317,6 +318,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
swImagesInline = view.findViewById(R.id.swImagesInline); swImagesInline = view.findViewById(R.id.swImagesInline);
swButtonExtra = view.findViewById(R.id.swButtonExtra); swButtonExtra = view.findViewById(R.id.swButtonExtra);
swUnzip = view.findViewById(R.id.swUnzip); swUnzip = view.findViewById(R.id.swUnzip);
tvUnzipHint = view.findViewById(R.id.tvUnzipHint);
swAttachmentsAlt = view.findViewById(R.id.swAttachmentsAlt); swAttachmentsAlt = view.findViewById(R.id.swAttachmentsAlt);
swThumbnails = view.findViewById(R.id.swThumbnails); swThumbnails = view.findViewById(R.id.swThumbnails);
swBundledFonts = view.findViewById(R.id.swBundledFonts); swBundledFonts = view.findViewById(R.id.swBundledFonts);
@ -1175,6 +1177,8 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
} }
}); });
tvUnzipHint.setText(getString(R.string.title_advanced_unzip_hint, MessageHelper.MAX_UNZIP));
swAttachmentsAlt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { swAttachmentsAlt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {

@ -49,6 +49,11 @@ import com.sun.mail.util.BASE64DecoderStream;
import com.sun.mail.util.FolderClosedIOException; import com.sun.mail.util.FolderClosedIOException;
import com.sun.mail.util.MessageRemovedIOException; import com.sun.mail.util.MessageRemovedIOException;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node; import org.jsoup.nodes.Node;
@ -144,6 +149,7 @@ public class MessageHelper {
static final String HEADER_CORRELATION_ID = "X-Correlation-ID"; static final String HEADER_CORRELATION_ID = "X-Correlation-ID";
static final int MAX_SUBJECT_AGE = 48; // hours static final int MAX_SUBJECT_AGE = 48; // hours
static final int DEFAULT_THREAD_RANGE = 7; // 2^7 = 128 days static final int DEFAULT_THREAD_RANGE = 7; // 2^7 = 128 days
static final int MAX_UNZIP = 10;
static final List<String> RECEIVED_WORDS = Collections.unmodifiableList(Arrays.asList( static final List<String> RECEIVED_WORDS = Collections.unmodifiableList(Arrays.asList(
"from", "by", "via", "with", "id", "for" "from", "by", "via", "with", "id", "for"
@ -3373,77 +3379,146 @@ public class MessageHelper {
} }
} catch (Throwable ex) { } catch (Throwable ex) {
Log.e(ex); Log.e(ex);
db.attachment().setWarning(local.id, Log.formatThrowable(ex));
} }
else if ("application/zip".equals(local.type)) { else if (local.isCompressed()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean unzip = prefs.getBoolean("unzip", false); boolean unzip = prefs.getBoolean("unzip", false);
if (unzip) { if (unzip)
// https://developer.android.com/reference/java/util/zip/ZipInputStream if (local.isGzip() && !local.isTarGzip())
try (ZipInputStream zis = new ZipInputStream( try (GzipCompressorInputStream gzip = new GzipCompressorInputStream(
new BufferedInputStream(new FileInputStream(local.getFile(context))))) { new BufferedInputStream(new FileInputStream(local.getFile(context))))) {
String name = gzip.getMetaData().getFilename();
int subsequence = 1; long total = gzip.getUncompressedCount();
ZipEntry ze; Log.i("Gzipped attachment seq=" + local.sequence + " " + name + ":" + total);
while ((ze = zis.getNextEntry()) != null)
try { if (name == null &&
String name = ze.getName(); local.name != null && local.name.endsWith(".gz"))
long total = ze.getSize(); name = local.name.substring(0, local.name.length() - 3);
// isDirectory: EntityAttachment attachment = new EntityAttachment();
// A directory entry is defined to be one whose name ends with a '/'. attachment.message = local.message;
if (ze.isDirectory() || attachment.sequence = local.sequence;
(name != null && name.endsWith("\\"))) { attachment.subsequence = 1;
Log.i("Zipped folder=" + name); attachment.name = name;
continue; attachment.type = Helper.guessMimeType(name);
if (total >= 0)
attachment.size = total;
attachment.id = db.attachment().insertAttachment(attachment);
File efile = attachment.getFile(context);
Log.i("Gunzipping to " + efile);
int last = 0;
long size = 0;
try (OutputStream os = new FileOutputStream(efile)) {
byte[] buffer = new byte[Helper.BUFFER_SIZE];
for (int len = gzip.read(buffer); len != -1; len = gzip.read(buffer)) {
size += len;
os.write(buffer, 0, len);
if (total > 0) {
int progress = (int) (size * 100 / total);
if (progress / 20 > last / 20) {
last = progress;
db.attachment().setProgress(attachment.id, progress);
}
}
} }
} catch (Throwable ex) {
Log.e(ex);
db.attachment().setError(attachment.id, Log.formatThrowable(ex));
db.attachment().setAvailable(attachment.id, true); // unrecoverable
}
Log.i("Zipped attachment seq=" + local.sequence + ":" + subsequence + db.attachment().setDownloaded(attachment.id, efile.length());
" " + name + ":" + total); } catch (Throwable ex) {
Log.e(ex);
EntityAttachment entry = new EntityAttachment(); db.attachment().setWarning(local.id, Log.formatThrowable(ex));
entry.message = local.message; }
entry.sequence = local.sequence; else
entry.subsequence = subsequence++; try (FileInputStream fis = new FileInputStream(local.getFile(context))) {
entry.name = name; ArchiveInputStream ais = new ArchiveStreamFactory().createArchiveInputStream(
entry.type = Helper.guessMimeType(entry.name); new BufferedInputStream(local.isTarGzip() ? new GzipCompressorInputStream(fis) : fis));
entry.size = total;
entry.id = db.attachment().insertAttachment(entry); int count = 0;
ArchiveEntry entry;
while ((entry = ais.getNextEntry()) != null)
if (ais.canReadEntryData(entry) && !entry.isDirectory())
if (++count > MAX_UNZIP)
break;
Log.i("Zip entries=" + count);
if (count <= MAX_UNZIP) {
fis.getChannel().position(0);
ais = new ArchiveStreamFactory().createArchiveInputStream(
new BufferedInputStream(local.isTarGzip() ? new GzipCompressorInputStream(fis) : fis));
int subsequence = 1;
while ((entry = ais.getNextEntry()) != null) {
if (!ais.canReadEntryData(entry)) {
Log.w("Zip invalid=" + entry);
continue;
}
File efile = entry.getFile(context); String name = entry.getName();
Log.i("Unzipping to " + efile); long total = entry.getSize();
int last = 0; if (entry.isDirectory() ||
long size = 0; (name != null && name.endsWith("\\"))) {
try (OutputStream os = new FileOutputStream(efile)) { Log.i("Zipped folder=" + name);
byte[] buffer = new byte[Helper.BUFFER_SIZE]; continue;
for (int len = zis.read(buffer); len != -1; len = zis.read(buffer)) { }
size += len;
os.write(buffer, 0, len);
int progress = (int) (size * 100 / total); Log.i("Zipped attachment seq=" + local.sequence + ":" + subsequence +
if (progress / 20 > last / 20) { " " + name + ":" + total);
last = progress;
db.attachment().setProgress(entry.id, progress); EntityAttachment attachment = new EntityAttachment();
attachment.message = local.message;
attachment.sequence = local.sequence;
attachment.subsequence = subsequence++;
attachment.name = name;
attachment.type = Helper.guessMimeType(name);
if (total >= 0)
attachment.size = total;
attachment.id = db.attachment().insertAttachment(attachment);
File efile = attachment.getFile(context);
Log.i("Unzipping to " + efile);
int last = 0;
long size = 0;
try (OutputStream os = new FileOutputStream(efile)) {
byte[] buffer = new byte[Helper.BUFFER_SIZE];
for (int len = ais.read(buffer); len != -1; len = ais.read(buffer)) {
size += len;
os.write(buffer, 0, len);
if (total > 0) {
int progress = (int) (size * 100 / total);
if (progress / 20 > last / 20) {
last = progress;
db.attachment().setProgress(attachment.id, progress);
}
}
} }
} catch (Throwable ex) {
Log.e(ex);
db.attachment().setError(attachment.id, Log.formatThrowable(ex));
db.attachment().setAvailable(attachment.id, true); // unrecoverable
} }
} catch (Throwable ex) {
Log.e(ex);
db.attachment().setError(entry.id, Log.formatThrowable(ex));
db.attachment().setAvailable(entry.id, true); // unrecoverable
}
db.attachment().setDownloaded(entry.id, efile.length()); db.attachment().setDownloaded(attachment.id, efile.length());
} finally { }
zis.closeEntry();
} }
} catch (Throwable ex) { } catch (Throwable ex) {
Log.e(ex); Log.e(ex);
db.attachment().setWarning(local.id, Log.formatThrowable(ex)); db.attachment().setWarning(local.id, Log.formatThrowable(ex));
} }
}
} }
} }
} }

@ -1761,6 +1761,18 @@
app:layout_constraintTop_toBottomOf="@id/swButtonExtra" app:layout_constraintTop_toBottomOf="@id/swButtonExtra"
app:switchPadding="12dp" /> app:switchPadding="12dp" />
<eu.faircode.email.FixedTextView
android:id="@+id/tvUnzipHint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="48dp"
android:text="@string/title_advanced_unzip_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/swUnzip" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/swAttachmentsAlt" android:id="@+id/swAttachmentsAlt"
android:layout_width="0dp" android:layout_width="0dp"
@ -1769,7 +1781,7 @@
android:text="@string/title_advanced_attachments_alt" android:text="@string/title_advanced_attachments_alt"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/swUnzip" app:layout_constraintTop_toBottomOf="@id/tvUnzipHint"
app:switchPadding="12dp" /> app:switchPadding="12dp" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat

@ -808,6 +808,7 @@
<string name="title_advanced_monospaced_pre_hint">Plain text only messages will be considered as preformatted</string> <string name="title_advanced_monospaced_pre_hint">Plain text only messages will be considered as preformatted</string>
<string name="title_advanced_placeholders_hint">This applies to reformatted messages only</string> <string name="title_advanced_placeholders_hint">This applies to reformatted messages only</string>
<string name="title_advanced_inline_hint">Inline images are images included in the message</string> <string name="title_advanced_inline_hint">Inline images are images included in the message</string>
<string name="title_advanced_unzip_hint">The contents of zip files with up to %1$d files will automatically be shown</string>
<string name="title_advanced_parse_classes_hint">This will more accurately display messages, but possibly with a delay</string> <string name="title_advanced_parse_classes_hint">This will more accurately display messages, but possibly with a delay</string>
<string name="title_advanced_language_detection_hint">Language detection support depends on the device manufacturer</string> <string name="title_advanced_language_detection_hint">Language detection support depends on the device manufacturer</string>

Loading…
Cancel
Save