Merge branch 'master' of github.com:M66B/FairEmail

This commit is contained in:
M66B 2023-04-08 14:17:15 +02:00
commit 9ef4b0a99a
43 changed files with 2950 additions and 806 deletions

View File

@ -6,6 +6,17 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
### [Saltopus](https://en.wikipedia.org/wiki/Saltopus)
### Next version
* Added filter rule groups
* Added executing of filter rules by automation apps, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq71)
* Added auto completion for reply template groups and filter rule groups
* Improved auto-discovery via DNS SRV records
* Small improvements and minor bug fixes
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2060 - 2023-04-01
* Added draft message printing

13
FAQ.md
View File

@ -2768,6 +2768,15 @@ The filter rules will move the messages to a (sub) archive folder as a second st
The POP3 protocol does not support setting keywords and moving or copying messages.
<br />
Since version 1.2061 it is possible to execute rules with an automation app, like for example Tasker.
```
(adb shell) am start-foreground-service -a eu.faircode.email.RULE --es account <account name> -e rule <unique rule name>
```
Using rules is a pro feature.
<br />
@ -3862,7 +3871,7 @@ FairEmail fetches a message in two steps:
Directly after the first step new messages will be notified.
However, only until after the second step the message text will be available.
FairEmail updates exiting notifications with a preview of the message text, but unfortunately wearable notifications cannot be updated.
FairEmail updates existing notifications with a preview of the message text, but unfortunately wearable notifications cannot be updated.
Since there is no guarantee that a message text will always be fetched directly after a message header,
it is not possible to guarantee that a new message notification with a preview text will always be sent to a wearable.
@ -4038,7 +4047,7 @@ Individual messages will rarely be trashed and mostly this happens by accident.
Showing trashed messages in conversations makes it easier to find them back.
You can permanently delete a message using the message three-dots *delete* menu, which will remove the message from the conversation.
Note that this irreversible.
Note that this is irreversible.
Similarly, drafts are shown in conversations to find them back in the context where they belong.
It is easy to read through the received messages before continuing to write the draft later.

View File

@ -14,7 +14,7 @@ FairEmail might be for you if you value your privacy.
<i>Almost all features are free to use, but to maintain and support the app in the long term, not every feature can be for free. See below for a list of pro features.</i>
<i>A lot of effort has gone into this app, which was developed to help you protect your privacy. If you have a question or problem, there is always support at marcel@faircode.eu.</i>
<i>A lot of effort has gone into this mail app, which was developed to help you protect your privacy. If you have a question or problem, there is always support at marcel@faircode.eu.</i>
<b>Main features</b>

View File

@ -439,18 +439,18 @@ dependencies {
def startup_version = "1.1.1" // 1.2.0-alpha02
def annotation_version_experimental = "1.3.0"
def core_version = "1.10.0-rc01" // 1.11.0-alpha01
def core_version = "1.10.0"
def appcompat_version = "1.6.1" // 1.7.0-alpha02
def emoji_version = "1.3.0" // 1.4.0-alpha01
def emoji_version = "1.3.0" // 1.4.0-beta01
def flatbuffers_version = "2.0.0"
def activity_version = "1.7.0"
def fragment_version = "1.5.6" // 1.6.0-alpha08
def windows_version = "1.0.0" // 1.1.0-alpha06
def webkit_version = "1.6.1" // 1.7.0-alpha03
def fragment_version = "1.5.6" // 1.6.0-alpha09
def windows_version = "1.0.0" // 1.1.0-beta02
def webkit_version = "1.6.1" // 1.7.0-beta01
def recyclerview_version = "1.3.0"
def coordinatorlayout_version = "1.2.0"
def constraintlayout_version = "2.1.4" // 2.2.0-alpha09
def material_version = "1.8.0"
def material_version = "1.9.0-beta01" // 1.10.0-alpha01
def browser_version = "1.5.0"
def lbm_version = "1.1.0"
def swiperefresh_version = "1.2.0-alpha01"

View File

@ -561,6 +561,7 @@
<action android:name="${applicationId}.ENABLE" />
<action android:name="${applicationId}.DISABLE" />
<action android:name="${applicationId}.INTERVAL" />
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
</intent-filter>
</service>

View File

@ -83,7 +83,7 @@ public class Avatar {
for (String dns : LIBRAVATAR_DNS.split(",")) {
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns + "." + domain, "srv");
if (records.length > 0) {
baseUrl = (records[0].port == 443 ? "https" : "http") + "://" + records[0].name + "/avatar/";
baseUrl = (records[0].port == 443 ? "https" : "http") + "://" + records[0].response + "/avatar/";
break;
}
}

View File

@ -101,7 +101,7 @@ public class Bimi {
}
// Process DNS record
Map<String, String> values = MessageHelper.getKeyValues(record.name);
Map<String, String> values = MessageHelper.getKeyValues(record.response);
List<String> tags = new ArrayList<>(values.keySet());
Collections.sort(tags); // process certificate first
for (String tag : tags) {
@ -337,9 +337,9 @@ public class Bimi {
}
if (records.length == 0)
throw new IllegalArgumentException("DMARC missing");
Log.i("BIMI got TXT " + records[0].name);
Log.i("BIMI got TXT " + records[0].response);
Map<String, String> dmarc = MessageHelper.getKeyValues(records[0].name);
Map<String, String> dmarc = MessageHelper.getKeyValues(records[0].response);
String p = dmarc.get("p");
if (p == null ||
@ -376,7 +376,7 @@ public class Bimi {
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, txt, "txt");
if (records.length == 0)
return null;
Log.i("BIMI got TXT " + records[0].name);
Log.i("BIMI got TXT " + records[0].response);
return records[0];
} catch (Throwable ex) {
Log.i(ex);

View File

@ -567,6 +567,7 @@
<action android:name="${applicationId}.ENABLE" />
<action android:name="${applicationId}.DISABLE" />
<action android:name="${applicationId}.INTERVAL" />
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
</intent-filter>
</service>

View File

@ -567,6 +567,7 @@
<action android:name="${applicationId}.ENABLE" />
<action android:name="${applicationId}.DISABLE" />
<action android:name="${applicationId}.INTERVAL" />
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
</intent-filter>
</service>

View File

@ -559,6 +559,7 @@
<action android:name="${applicationId}.ENABLE" />
<action android:name="${applicationId}.DISABLE" />
<action android:name="${applicationId}.INTERVAL" />
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
</intent-filter>
</service>

View File

@ -6,6 +6,17 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
### [Saltopus](https://en.wikipedia.org/wiki/Saltopus)
### Next version
* Added filter rule groups
* Added executing of filter rules by automation apps, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq71)
* Added auto completion for reply template groups and filter rule groups
* Improved auto-discovery via DNS SRV records
* Small improvements and minor bug fixes
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2060 - 2023-04-01
* Added draft message printing

View File

@ -1051,8 +1051,7 @@ fm
// fo : https://en.wikipedia.org/wiki/.fo
fo
// fr : http://www.afnic.fr/
// domaines descriptifs : https://www.afnic.fr/medias/documents/Cadre_legal/Afnic_Naming_Policy_12122016_VEN.pdf
// fr : https://www.afnic.fr/ https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
fr
asso.fr
com.fr
@ -1060,7 +1059,7 @@ gouv.fr
nom.fr
prd.fr
tm.fr
// domaines sectoriels : https://www.afnic.fr/en/products-and-services/the-fr-tld/sector-based-fr-domains-4.html
// Former "domaines sectoriels", still registration suffixes
aeroport.fr
avocat.fr
avoues.fr
@ -4070,555 +4069,8 @@ ac.mu
co.mu
or.mu
// museum : http://about.museum/naming/
// http://index.museum/
// museum : https://welcome.museum/wp-content/uploads/2018/05/20180525-Registration-Policy-MUSEUM-EN_VF-2.pdf https://welcome.museum/buy-your-dot-museum-2/
museum
academy.museum
agriculture.museum
air.museum
airguard.museum
alabama.museum
alaska.museum
amber.museum
ambulance.museum
american.museum
americana.museum
americanantiques.museum
americanart.museum
amsterdam.museum
and.museum
annefrank.museum
anthro.museum
anthropology.museum
antiques.museum
aquarium.museum
arboretum.museum
archaeological.museum
archaeology.museum
architecture.museum
art.museum
artanddesign.museum
artcenter.museum
artdeco.museum
arteducation.museum
artgallery.museum
arts.museum
artsandcrafts.museum
asmatart.museum
assassination.museum
assisi.museum
association.museum
astronomy.museum
atlanta.museum
austin.museum
australia.museum
automotive.museum
aviation.museum
axis.museum
badajoz.museum
baghdad.museum
bahn.museum
bale.museum
baltimore.museum
barcelona.museum
baseball.museum
basel.museum
baths.museum
bauern.museum
beauxarts.museum
beeldengeluid.museum
bellevue.museum
bergbau.museum
berkeley.museum
berlin.museum
bern.museum
bible.museum
bilbao.museum
bill.museum
birdart.museum
birthplace.museum
bonn.museum
boston.museum
botanical.museum
botanicalgarden.museum
botanicgarden.museum
botany.museum
brandywinevalley.museum
brasil.museum
bristol.museum
british.museum
britishcolumbia.museum
broadcast.museum
brunel.museum
brussel.museum
brussels.museum
bruxelles.museum
building.museum
burghof.museum
bus.museum
bushey.museum
cadaques.museum
california.museum
cambridge.museum
can.museum
canada.museum
capebreton.museum
carrier.museum
cartoonart.museum
casadelamoneda.museum
castle.museum
castres.museum
celtic.museum
center.museum
chattanooga.museum
cheltenham.museum
chesapeakebay.museum
chicago.museum
children.museum
childrens.museum
childrensgarden.museum
chiropractic.museum
chocolate.museum
christiansburg.museum
cincinnati.museum
cinema.museum
circus.museum
civilisation.museum
civilization.museum
civilwar.museum
clinton.museum
clock.museum
coal.museum
coastaldefence.museum
cody.museum
coldwar.museum
collection.museum
colonialwilliamsburg.museum
coloradoplateau.museum
columbia.museum
columbus.museum
communication.museum
communications.museum
community.museum
computer.museum
computerhistory.museum
comunicações.museum
contemporary.museum
contemporaryart.museum
convent.museum
copenhagen.museum
corporation.museum
correios-e-telecomunicações.museum
corvette.museum
costume.museum
countryestate.museum
county.museum
crafts.museum
cranbrook.museum
creation.museum
cultural.museum
culturalcenter.museum
culture.museum
cyber.museum
cymru.museum
dali.museum
dallas.museum
database.museum
ddr.museum
decorativearts.museum
delaware.museum
delmenhorst.museum
denmark.museum
depot.museum
design.museum
detroit.museum
dinosaur.museum
discovery.museum
dolls.museum
donostia.museum
durham.museum
eastafrica.museum
eastcoast.museum
education.museum
educational.museum
egyptian.museum
eisenbahn.museum
elburg.museum
elvendrell.museum
embroidery.museum
encyclopedic.museum
england.museum
entomology.museum
environment.museum
environmentalconservation.museum
epilepsy.museum
essex.museum
estate.museum
ethnology.museum
exeter.museum
exhibition.museum
family.museum
farm.museum
farmequipment.museum
farmers.museum
farmstead.museum
field.museum
figueres.museum
filatelia.museum
film.museum
fineart.museum
finearts.museum
finland.museum
flanders.museum
florida.museum
force.museum
fortmissoula.museum
fortworth.museum
foundation.museum
francaise.museum
frankfurt.museum
franziskaner.museum
freemasonry.museum
freiburg.museum
fribourg.museum
frog.museum
fundacio.museum
furniture.museum
gallery.museum
garden.museum
gateway.museum
geelvinck.museum
gemological.museum
geology.museum
georgia.museum
giessen.museum
glas.museum
glass.museum
gorge.museum
grandrapids.museum
graz.museum
guernsey.museum
halloffame.museum
hamburg.museum
handson.museum
harvestcelebration.museum
hawaii.museum
health.museum
heimatunduhren.museum
hellas.museum
helsinki.museum
hembygdsforbund.museum
heritage.museum
histoire.museum
historical.museum
historicalsociety.museum
historichouses.museum
historisch.museum
historisches.museum
history.museum
historyofscience.museum
horology.museum
house.museum
humanities.museum
illustration.museum
imageandsound.museum
indian.museum
indiana.museum
indianapolis.museum
indianmarket.museum
intelligence.museum
interactive.museum
iraq.museum
iron.museum
isleofman.museum
jamison.museum
jefferson.museum
jerusalem.museum
jewelry.museum
jewish.museum
jewishart.museum
jfk.museum
journalism.museum
judaica.museum
judygarland.museum
juedisches.museum
juif.museum
karate.museum
karikatur.museum
kids.museum
koebenhavn.museum
koeln.museum
kunst.museum
kunstsammlung.museum
kunstunddesign.museum
labor.museum
labour.museum
lajolla.museum
lancashire.museum
landes.museum
lans.museum
läns.museum
larsson.museum
lewismiller.museum
lincoln.museum
linz.museum
living.museum
livinghistory.museum
localhistory.museum
london.museum
losangeles.museum
louvre.museum
loyalist.museum
lucerne.museum
luxembourg.museum
luzern.museum
mad.museum
madrid.museum
mallorca.museum
manchester.museum
mansion.museum
mansions.museum
manx.museum
marburg.museum
maritime.museum
maritimo.museum
maryland.museum
marylhurst.museum
media.museum
medical.museum
medizinhistorisches.museum
meeres.museum
memorial.museum
mesaverde.museum
michigan.museum
midatlantic.museum
military.museum
mill.museum
miners.museum
mining.museum
minnesota.museum
missile.museum
missoula.museum
modern.museum
moma.museum
money.museum
monmouth.museum
monticello.museum
montreal.museum
moscow.museum
motorcycle.museum
muenchen.museum
muenster.museum
mulhouse.museum
muncie.museum
museet.museum
museumcenter.museum
museumvereniging.museum
music.museum
national.museum
nationalfirearms.museum
nationalheritage.museum
nativeamerican.museum
naturalhistory.museum
naturalhistorymuseum.museum
naturalsciences.museum
nature.museum
naturhistorisches.museum
natuurwetenschappen.museum
naumburg.museum
naval.museum
nebraska.museum
neues.museum
newhampshire.museum
newjersey.museum
newmexico.museum
newport.museum
newspaper.museum
newyork.museum
niepce.museum
norfolk.museum
north.museum
nrw.museum
nyc.museum
nyny.museum
oceanographic.museum
oceanographique.museum
omaha.museum
online.museum
ontario.museum
openair.museum
oregon.museum
oregontrail.museum
otago.museum
oxford.museum
pacific.museum
paderborn.museum
palace.museum
paleo.museum
palmsprings.museum
panama.museum
paris.museum
pasadena.museum
pharmacy.museum
philadelphia.museum
philadelphiaarea.museum
philately.museum
phoenix.museum
photography.museum
pilots.museum
pittsburgh.museum
planetarium.museum
plantation.museum
plants.museum
plaza.museum
portal.museum
portland.museum
portlligat.museum
posts-and-telecommunications.museum
preservation.museum
presidio.museum
press.museum
project.museum
public.museum
pubol.museum
quebec.museum
railroad.museum
railway.museum
research.museum
resistance.museum
riodejaneiro.museum
rochester.museum
rockart.museum
roma.museum
russia.museum
saintlouis.museum
salem.museum
salvadordali.museum
salzburg.museum
sandiego.museum
sanfrancisco.museum
santabarbara.museum
santacruz.museum
santafe.museum
saskatchewan.museum
satx.museum
savannahga.museum
schlesisches.museum
schoenbrunn.museum
schokoladen.museum
school.museum
schweiz.museum
science.museum
scienceandhistory.museum
scienceandindustry.museum
sciencecenter.museum
sciencecenters.museum
science-fiction.museum
sciencehistory.museum
sciences.museum
sciencesnaturelles.museum
scotland.museum
seaport.museum
settlement.museum
settlers.museum
shell.museum
sherbrooke.museum
sibenik.museum
silk.museum
ski.museum
skole.museum
society.museum
sologne.museum
soundandvision.museum
southcarolina.museum
southwest.museum
space.museum
spy.museum
square.museum
stadt.museum
stalbans.museum
starnberg.museum
state.museum
stateofdelaware.museum
station.museum
steam.museum
steiermark.museum
stjohn.museum
stockholm.museum
stpetersburg.museum
stuttgart.museum
suisse.museum
surgeonshall.museum
surrey.museum
svizzera.museum
sweden.museum
sydney.museum
tank.museum
tcm.museum
technology.museum
telekommunikation.museum
television.museum
texas.museum
textile.museum
theater.museum
time.museum
timekeeping.museum
topology.museum
torino.museum
touch.museum
town.museum
transport.museum
tree.museum
trolley.museum
trust.museum
trustee.museum
uhren.museum
ulm.museum
undersea.museum
university.museum
usa.museum
usantiques.museum
usarts.museum
uscountryestate.museum
usculture.museum
usdecorativearts.museum
usgarden.museum
ushistory.museum
ushuaia.museum
uslivinghistory.museum
utah.museum
uvic.museum
valley.museum
vantaa.museum
versailles.museum
viking.museum
village.museum
virginia.museum
virtual.museum
virtuel.museum
vlaanderen.museum
volkenkunde.museum
wales.museum
wallonie.museum
war.museum
washingtondc.museum
watchandclock.museum
watch-and-clock.museum
western.museum
westfalen.museum
whaling.museum
wildlife.museum
williamsburg.museum
windmill.museum
workshop.museum
york.museum
yorkshire.museum
yosemite.museum
youth.museum
zoological.museum
zoology.museum
ירושלים.museum
иком.museum
// mv : https://en.wikipedia.org/wiki/.mv
// "mv" included because, contra Wikipedia, google.mv exists.
@ -5861,7 +5313,7 @@ zarow.pl
zgora.pl
zgorzelec.pl
// pm : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf
// pm : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
pm
// pn : http://www.government.pn/PnRegistry/policies.htm
@ -5959,7 +5411,7 @@ net.qa
org.qa
sch.qa
// re : http://www.afnic.re/obtenir/chartes/nommage-re/annexe-descriptifs
// re : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
re
asso.re
com.re
@ -6216,7 +5668,7 @@ td
// http://www.telnic.org/
tel
// tf : https://en.wikipedia.org/wiki/.tf
// tf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
tf
// tg : https://en.wikipedia.org/wiki/.tg
@ -6835,7 +6287,7 @@ edu.vu
net.vu
org.vu
// wf : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf
// wf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
wf
// ws : https://en.wikipedia.org/wiki/.ws
@ -6847,7 +6299,7 @@ org.ws
gov.ws
edu.ws
// yt : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf
// yt : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf
yt
// IDN ccTLDs
@ -13459,6 +12911,10 @@ qoto.io
// Submitted by Xavier De Cock <xdecock@gmail.com>
qualifioapp.com
// Quality Unit: https://qualityunit.com
// Submitted by Vasyl Tsalko <vtsalko@qualityunit.com>
ladesk.com
// QuickBackend: https://www.quickbackend.com
// Submitted by Dani Biro <dani@pymet.com>
qbuser.com

View File

@ -268,7 +268,7 @@ public class ActivityDmarc extends ActivityBase {
boolean valid = false;
if (spf != null)
for (Pair<String, DnsHelper.DnsRecord> p : spf) {
for (String ip : p.second.name.split("\\s+")) {
for (String ip : p.second.response.split("\\s+")) {
ip = ip.toLowerCase(Locale.ROOT);
if (ip.startsWith("ip4:") || ip.startsWith("ip6:")) {
String[] net = ip.substring(4).split("/");
@ -290,15 +290,15 @@ public class ActivityDmarc extends ActivityBase {
for (DnsHelper.DnsRecord mx : mxs) {
List<DnsHelper.DnsRecord> as = new ArrayList<>();
try {
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.name, "a")));
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.response, "a")));
} catch (UnknownHostException ignored) {
}
try {
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.name, "aaaa")));
as.addAll(Arrays.asList(DnsHelper.lookup(context, mx.response, "aaaa")));
} catch (UnknownHostException ignored) {
}
for (DnsHelper.DnsRecord a : as)
if (text.equals(a.name)) {
if (text.equals(a.response)) {
valid = true;
break;
}
@ -431,7 +431,7 @@ public class ActivityDmarc extends ActivityBase {
spf = lookupSpf(context, lastDomain, extra);
for (Pair<String, DnsHelper.DnsRecord> p : spf) {
ssb.append(p.first).append(' ')
.append(p.second.name).append("\n");
.append(p.second.response).append("\n");
if (start == null) {
start = ssb.length();
ssb.append("\n");
@ -455,7 +455,7 @@ public class ActivityDmarc extends ActivityBase {
} catch (UnknownHostException ignored) {
}
for (DnsHelper.DnsRecord r : records)
ssb.append(r.name).append("\n");
ssb.append(r.response).append("\n");
ssb.append("\n");
}
}
@ -528,9 +528,9 @@ public class ActivityDmarc extends ActivityBase {
ssb.append(domain).append('=')
.append(Integer.toString(records.length)).append('\n');
for (DnsHelper.DnsRecord r : records)
if (r.name.contains("spf")) {
if (r.response.contains("spf")) {
result.add(new Pair<>(domain, r));
for (String part : r.name.split("\\s+"))
for (String part : r.response.split("\\s+"))
if (part.toLowerCase(Locale.ROOT).startsWith("include:")) {
String sub = part.substring("include:".length());
result.addAll(lookupSpf(context, sub, ssb));

View File

@ -2839,8 +2839,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
@Override
public void onScrollChange(int scrollX, int scrollY) {
properties.setPosition(message.id, new Pair<Integer, Integer>(scrollX, scrollY));
public void onScrollChange(int dx, int dy, int scrollX, int scrollY) {
properties.setPosition(message.id, new Pair<>(dx, dy), new Pair<>(scrollX, scrollY));
}
@Override
@ -4500,7 +4500,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
properties.setSize(message.id, null);
properties.setHeight(message.id, null);
properties.setPosition(message.id, null);
properties.setPosition(message.id, null, null);
if (itemId == R.string.title_fit_width && wvBody instanceof WebView)
((WebView) wvBody).getSettings().setLoadWithOverviewMode(enabled);
@ -5371,7 +5371,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
properties.setSize(message.id, null);
properties.setHeight(message.id, null);
properties.setPosition(message.id, null);
properties.setPosition(message.id, null, null);
if (full)
setupTools(message, false, false);
@ -6589,7 +6589,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
public boolean onMenuItemClick(MenuItem item) {
properties.setSize(message.id, null);
properties.setHeight(message.id, null);
properties.setPosition(message.id, null);
properties.setPosition(message.id, null, null);
args.putString("charset", item.getIntent().getStringExtra("charset"));
@ -6642,7 +6642,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private void onMenuAlt(TupleMessageEx message) {
properties.setSize(message.id, null);
properties.setHeight(message.id, null);
properties.setPosition(message.id, null);
properties.setPosition(message.id, null, null);
Bundle args = new Bundle();
args.putLong("id", message.id);
@ -8571,7 +8571,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
int getHeight(long id, int defaultHeight);
void setPosition(long id, Pair<Integer, Integer> position);
void setPosition(long id, Pair<Integer, Integer> delta, Pair<Integer, Integer> position);
Pair<Integer, Integer> getPosition(long id);

View File

@ -3014,7 +3014,7 @@ class Core {
imessages.length > 0 && folder.last_sync_count != null &&
imessages.length == folder.last_sync_count) {
// Check if last message known as new messages indicator
MessageHelper helper = new MessageHelper((MimeMessage) imessages[imessages.length - 1], context);
MessageHelper helper = new MessageHelper((MimeMessage) imessages[reversed ? 0 : imessages.length - 1], context);
String msgid = helper.getPOP3MessageID();
if (msgid != null) {
int count = db.message().countMessageByMsgId(folder.id, msgid, true);
@ -3025,9 +3025,33 @@ class Core {
}
}
// Index IDs
int flagged = 0;
Map<String, TupleUidl> uidlTuple = new HashMap<>();
Map<String, TupleUidl> msgIdTuple = new HashMap<>();
for (TupleUidl id : ids) {
if (id.ui_flagged && !id.ui_hide)
flagged++;
if (id.uidl != null) {
if (uidlTuple.containsKey(id.uidl))
Log.w(account.name + " POP duplicate uidl/msgid=" + id.uidl + "/" + id.msgid);
uidlTuple.put(id.uidl, id);
}
if (id.msgid != null) {
if (msgIdTuple.containsKey(id.msgid))
Log.w(account.name + " POP duplicate msgid/uidl=" + id.msgid + "/" + id.uidl);
msgIdTuple.put(id.msgid, id);
}
}
max = Math.min(max + flagged, imessages.length);
EntityLog.log(context, account.name + " POP" +
" device=" + ids.size() +
" server=" + imessages.length +
" flagged=" + flagged +
" max=" + max + "/" + account.max_messages +
" reversed=" + reversed +
" last=" + folder.last_sync_count +
@ -3035,24 +3059,6 @@ class Core {
" uidl=" + hasUidl);
if (sync) {
// Index IDs
Map<String, TupleUidl> uidlTuple = new HashMap<>();
for (TupleUidl id : ids) {
if (id.uidl != null) {
if (uidlTuple.containsKey(id.uidl))
Log.w(account.name + " POP duplicate uidl/msgid=" + id.uidl + "/" + id.msgid);
uidlTuple.put(id.uidl, id);
}
}
Map<String, TupleUidl> msgIdTuple = new HashMap<>();
for (TupleUidl id : ids)
if (id.msgid != null) {
if (msgIdTuple.containsKey(id.msgid))
Log.w(account.name + " POP duplicate msgid/uidl=" + id.msgid + "/" + id.uidl);
msgIdTuple.put(id.msgid, id);
}
// Fetch UIDLs
if (hasUidl) {
FetchProfile ifetch = new FetchProfile();
@ -3427,8 +3433,9 @@ class Core {
int hidden = db.message().setMessagesUiHide(folder.id, Math.abs(account.max_messages));
int deleted = db.message().deleteMessagesKeep(folder.id, Math.abs(account.max_messages) + 100);
EntityLog.log(context, account.name + " POP" +
" cleanup max=" + account.max_messages + "" +
" hidden=" + hidden + " deleted=" + deleted);
" cleanup max=" + account.max_messages +
" hidden=" + hidden +
" deleted=" + deleted);
}
folder.last_sync_count = imessages.length;
@ -5256,6 +5263,13 @@ class Core {
" ignored=" + message.ui_ignored +
" hide=" + message.ui_hide);
else {
// Prevent reappearing notifications
EntityMessage msg = db.message().getMessage(message.id);
if (msg == null || msg.ui_ignored) {
Log.i("Notify skip id=" + message.id + " msg=" + (msg != null));
continue;
}
Integer current = newMessages.get(group);
newMessages.put(group, current == null ? 1 : current + 1);
@ -5289,11 +5303,6 @@ class Core {
remove.remove(id);
Log.i("Notify existing=" + id);
} else {
EntityMessage msg = db.message().getMessage(message.id);
if (msg == null || msg.ui_ignored) {
Log.i("Notify skip id=" + message.id + " msg=" + (msg != null));
continue;
}
boolean existing = remove.contains(-id);
if (existing) {
if (message.content && notify_preview) {

View File

@ -74,6 +74,11 @@ public interface DaoAnswer {
" AND (:favorite OR NOT favorite)")
Integer getAnswerCount(boolean favorite);
@Query("SELECT DISTINCT `group` FROM answer" +
" WHERE NOT `group` IS NULL" +
" ORDER by `group` COLLATE NOCASE")
List<String> getGroups();
@Insert
long insertAnswer(EntityAnswer answer);

View File

@ -789,6 +789,7 @@ public interface DaoMessage {
@Transaction
@Query("UPDATE message SET ui_hide = 1" +
" WHERE folder = :folder" +
" AND NOT ui_flagged" +
" AND id NOT IN (" +
" SELECT id FROM message" +
" WHERE folder = :folder" +
@ -1015,6 +1016,7 @@ public interface DaoMessage {
@Transaction
@Query("DELETE FROM message" +
" WHERE folder = :folder" +
" AND NOT ui_flagged" +
" AND id NOT IN (" +
" SELECT id FROM message" +
" WHERE folder = :folder" +

View File

@ -47,6 +47,12 @@ public interface DaoRule {
" WHERE rule.id = :id")
TupleRuleEx getRule(long id);
@Query("SELECT rule.* FROM rule" +
" JOIN folder ON folder.id = rule.folder" +
" WHERE folder.account = :account" +
" AND rule.name = :name")
List<EntityRule> getRuleByName(long account, String name);
@Query("SELECT * FROM rule WHERE uuid = :uuid")
EntityRule getRuleByUUID(String uuid);
@ -56,6 +62,11 @@ public interface DaoRule {
" WHERE rule.folder = :folder")
LiveData<List<TupleRuleEx>> liveRules(long folder);
@Query("SELECT DISTINCT `group` FROM rule" +
" WHERE NOT `group` IS NULL" +
" ORDER by `group` COLLATE NOCASE")
List<String> getGroups();
@Query("SELECT COUNT(*) FROM rule")
int countTotal();

View File

@ -210,7 +210,7 @@ public class DnsHelper {
result.add(new DnsRecord(soa.getHost().toString(true)));
} else if (record instanceof SRVRecord) {
SRVRecord srv = (SRVRecord) record;
result.add(new DnsRecord(srv.getTarget().toString(true), srv.getPort()));
result.add(new DnsRecord(srv.getTarget().toString(true), srv.getPort(), srv.getPriority(), srv.getWeight()));
} else if (record instanceof TXTRecord) {
TXTRecord txt = (TXTRecord) record;
for (Object content : txt.getStrings()) {
@ -227,7 +227,7 @@ public class DnsHelper {
slash = text.indexOf('\\', i);
}
if (result.size() > 0)
result.get(0).name += text;
result.get(0).response += text;
else
result.add(new DnsRecord(text, 0));
}
@ -241,6 +241,9 @@ public class DnsHelper {
throw new IllegalArgumentException(record.getClass().getName());
}
for (DnsRecord record : result)
record.query = name;
return result.toArray(new DnsRecord[0]);
} catch (TextParseException ex) {
Log.e(ex);
@ -283,16 +286,32 @@ public class DnsHelper {
}
static class DnsRecord {
String name;
String query;
String response;
Integer port;
Integer priority;
Integer weight;
DnsRecord(String name) {
this.name = name;
DnsRecord(String response) {
this.response = response;
}
DnsRecord(String name, int port) {
this.name = name;
DnsRecord(String response, int port) {
this.response = response;
this.port = port;
}
DnsRecord(String response, int port, int priority, int weight) {
this.response = response;
this.port = port;
this.priority = priority;
this.weight = weight;
}
@NonNull
@Override
public String toString() {
return query + "=" + response + ":" + port + " " + priority + "/" + weight;
}
}
}

View File

@ -0,0 +1,203 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2023 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.textclassifier.TextClassifier;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
public class EditTextAutoComplete extends AppCompatAutoCompleteTextView {
public EditTextAutoComplete(@NonNull Context context) {
super(context);
}
public EditTextAutoComplete(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public EditTextAutoComplete(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void setSelection(int index) {
try {
super.setSelection(index);
} catch (Throwable ex) {
Log.e(ex);
}
}
@Override
public void setSelection(int start, int stop) {
try {
super.setSelection(start, stop);
} catch (Throwable ex) {
Log.e(ex);
}
}
@Override
public void setText(CharSequence text, BufferType type) {
try {
super.setText(text, type);
} catch (Throwable ex) {
Log.w(ex);
}
}
@Override
public boolean onPreDraw() {
try {
return super.onPreDraw();
} catch (Throwable ex) {
Log.w(ex);
return true;
}
}
@Override
protected void onDraw(Canvas canvas) {
try {
super.onDraw(canvas);
} catch (Throwable ex) {
Log.w(ex);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
try {
return super.dispatchTouchEvent(event);
} catch (Throwable ex) {
Log.w(ex);
return false;
}
}
@Override
public void dispatchWindowFocusChanged(boolean hasFocus) {
try {
super.dispatchWindowFocusChanged(hasFocus);
} catch (Throwable ex) {
Log.w(ex);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
return super.onTouchEvent(event);
} catch (Throwable ex) {
Log.w(ex);
return false;
}
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
try {
return super.onKeyPreIme(keyCode, event);
} catch (Throwable ex) {
Log.w(ex);
return false;
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
try {
return super.onKeyUp(keyCode, event);
} catch (Throwable ex) {
Log.w(ex);
return true;
}
}
@Override
public boolean performClick() {
try {
return super.performClick();
} catch (Throwable ex) {
Log.w(ex);
return false;
}
}
@Override
public boolean performLongClick() {
try {
return super.performLongClick();
} catch (Throwable ex) {
Log.w(ex);
return false;
}
}
@Override
public ActionMode startActionMode(ActionMode.Callback callback) {
try {
return super.startActionMode(callback);
} catch (Throwable ex) {
Log.e(ex);
return null;
}
}
@Override
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
try {
return super.startActionMode(callback, type);
} catch (Throwable ex) {
Log.e(ex);
return null;
}
}
@NonNull
@Override
@RequiresApi(api = Build.VERSION_CODES.O)
public TextClassifier getTextClassifier() {
if (BuildConfig.DEBUG /*|| Helper.isSamsung()*/)
return TextClassifier.NO_OP;
else
return super.getTextClassifier();
}
@Override
public boolean onTextContextMenuItem(int id) {
try {
return super.onTextContextMenuItem(id);
} catch (Throwable ex) {
Log.e(ex);
return false;
}
}
}

View File

@ -427,7 +427,7 @@ public class EmailProvider implements Parcelable {
for (EmailProvider provider : providers)
if (provider.mx != null)
for (String mx : provider.mx)
if (record.name.matches(mx))
if (record.response.matches(mx))
return Arrays.asList(provider);
} catch (Throwable ex) {
Log.w(ex);
@ -477,8 +477,8 @@ public class EmailProvider implements Parcelable {
}
for (DnsHelper.DnsRecord record : records)
if (!TextUtils.isEmpty(record.name)) {
String target = record.name.toLowerCase(Locale.ROOT);
if (!TextUtils.isEmpty(record.response)) {
String target = record.response.toLowerCase(Locale.ROOT);
EntityLog.log(context, "MX target=" + target);
for (EmailProvider provider : providers) {
@ -510,7 +510,7 @@ public class EmailProvider implements Parcelable {
for (DnsHelper.DnsRecord record : records)
try {
String target = record.name.toLowerCase(Locale.ROOT);
String target = record.response.toLowerCase(Locale.ROOT);
InetAddress.getByName(target);
EmailProvider mx1 = new EmailProvider(domain);
@ -857,56 +857,81 @@ public class EmailProvider implements Parcelable {
EmailProvider provider = new EmailProvider(domain);
if (discover == Discover.ALL || discover == Discover.IMAP) {
try {
// Identifies an IMAP server where TLS is initiated directly upon connection to the IMAP server.
intf.onStatus("SRV imaps " + domain);
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_imaps._tcp." + domain, "srv");
if (records.length == 0)
throw new UnknownHostException(domain);
// ... service is not supported at all at a particular domain by setting the target of an SRV RR to "."
provider.imap.score = 50;
provider.imap.host = records[0].name;
provider.imap.port = records[0].port;
provider.imap.starttls = false;
EntityLog.log(context, "_imaps._tcp." + domain + "=" + provider.imap);
} catch (UnknownHostException ignored) {
// Identifies an IMAP server that MAY ... require the MUA to use the "STARTTLS" command
intf.onStatus("SRV imap " + domain);
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_imap._tcp." + domain, "srv");
if (records.length == 0)
throw new UnknownHostException(domain);
provider.imap.score = 50;
provider.imap.host = records[0].name;
provider.imap.port = records[0].port;
provider.imap.starttls = (provider.imap.port == 143);
EntityLog.log(context, "_imap._tcp." + domain + "=" + provider.imap);
}
intf.onStatus("SRV imap " + domain);
// Identifies an IMAP server where TLS is initiated directly upon connection to the IMAP server.
List<DnsHelper.DnsRecord> list = new ArrayList<>();
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_imap._tcp." + domain, "srv")));
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_imaps._tcp." + domain, "srv")));
// ... service is not supported at all at a particular domain by setting the target of an SRV RR to "."
for (DnsHelper.DnsRecord record : new ArrayList<>(list))
if (TextUtils.isEmpty(record.response) || ".".equals(record.response))
list.remove(record);
if (list.size() == 0)
throw new UnknownHostException(domain);
Collections.sort(list, new Comparator<DnsHelper.DnsRecord>() {
@Override
public int compare(DnsHelper.DnsRecord d1, DnsHelper.DnsRecord d2) {
int p = -Integer.compare(d1.priority, d2.priority);
if (p != 0)
return p;
int w = -Integer.compare(d1.weight, d2.weight);
if (w != 0)
return w;
return -Boolean.compare(d1.query.startsWith("_imaps._tcp."), d2.query.startsWith("_imaps._tcp."));
}
});
DnsHelper.DnsRecord pref = list.get(0);
provider.imap.score = 50;
provider.imap.host = pref.response;
provider.imap.port = pref.port;
provider.imap.starttls = (!pref.query.startsWith("_imaps._tcp.") && pref.port == 143);
EntityLog.log(context, pref.query + "=" + provider.imap);
}
if (discover == Discover.ALL || discover == Discover.SMTP)
try {
// Note that this covers connections both with and without Transport Layer Security (TLS)
intf.onStatus("SRV smtp " + domain);
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_submission._tcp." + domain, "srv");
if (records.length == 0)
throw new UnknownHostException(domain);
provider.smtp.score = 50;
provider.smtp.host = records[0].name;
provider.smtp.port = records[0].port;
provider.smtp.starttls = (provider.smtp.port == 587);
EntityLog.log(context, "_submission._tcp." + domain + "=" + provider.smtp);
} catch (UnknownHostException ignored) {
// https://tools.ietf.org/html/rfc8314
intf.onStatus("SRV smtps " + domain);
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, "_submissions._tcp." + domain, "srv");
if (records.length == 0)
throw new UnknownHostException(domain);
provider.smtp.score = 50;
provider.smtp.host = records[0].name;
provider.smtp.port = records[0].port;
provider.smtp.starttls = false;
EntityLog.log(context, "_submissions._tcp." + domain + "=" + provider.smtp);
}
if (discover == Discover.ALL || discover == Discover.SMTP) {
intf.onStatus("SRV smtp " + domain);
// https://tools.ietf.org/html/rfc8314
List<DnsHelper.DnsRecord> list = new ArrayList<>();
// Note that this covers connections both with and without Transport Layer Security (TLS)
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_submission._tcp." + domain, "srv")));
list.addAll(Arrays.asList(DnsHelper.lookup(context, "_submissions._tcp." + domain, "srv")));
for (DnsHelper.DnsRecord record : new ArrayList<>(list))
if (TextUtils.isEmpty(record.response) || ".".equals(record.response))
list.remove(record);
if (list.size() == 0)
throw new UnknownHostException(domain);
Collections.sort(list, new Comparator<DnsHelper.DnsRecord>() {
@Override
public int compare(DnsHelper.DnsRecord d1, DnsHelper.DnsRecord d2) {
int p = -Integer.compare(d1.priority, d2.priority);
if (p != 0)
return p;
int w = -Integer.compare(d1.weight, d2.weight);
if (w != 0)
return w;
// submission is being preferred
return -Boolean.compare(d1.query.startsWith("_submission._tcp."), d2.query.startsWith("_submission._tcp."));
}
});
DnsHelper.DnsRecord pref = list.get(0);
provider.smtp.score = 50;
provider.smtp.host = pref.response;
provider.smtp.port = pref.port;
provider.smtp.starttls = (!pref.query.startsWith("_submissions._tcp.") && pref.port == 587);
EntityLog.log(context, pref.query + "=" + provider.smtp);
}
provider.validate();

View File

@ -206,6 +206,7 @@ public class EntityFolder extends EntityOrder implements Serializable {
put("archief", new TypeScore(EntityFolder.ARCHIVE, 100)); // Dutch
put("Архив", new TypeScore(EntityFolder.ARCHIVE, 100));
put("Wszystkie", new TypeScore(EntityFolder.ARCHIVE, 100)); // Polish
put("Arkiv", new TypeScore(EntityFolder.ARCHIVE, 100)); // Norwegian
put("draft", new TypeScore(EntityFolder.DRAFTS, 100));
put("concept", new TypeScore(EntityFolder.DRAFTS, 100));

View File

@ -42,6 +42,8 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
@ -58,13 +60,14 @@ import com.google.android.material.snackbar.Snackbar;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.util.ArrayList;
import java.util.List;
public class FragmentAnswer extends FragmentBase {
private ViewGroup view;
private EditText etName;
private EditText etLabel;
private EditText etGroup;
private AutoCompleteTextView etGroup;
private CheckBox cbStandard;
private CheckBox cbReceipt;
private CheckBox cbFavorite;
@ -78,6 +81,8 @@ public class FragmentAnswer extends FragmentBase {
private ContentLoadingProgressBar pbWait;
private Group grpReady;
private ArrayAdapter<String> adapterGroup;
private long id = -1;
private long copy = -1;
@ -133,6 +138,10 @@ public class FragmentAnswer extends FragmentBase {
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
adapterGroup = new ArrayAdapter<>(getContext(), R.layout.spinner_item1_dropdown, android.R.id.text1);
etGroup.setThreshold(1);
etGroup.setAdapter(adapterGroup);
btnColor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -248,6 +257,8 @@ public class FragmentAnswer extends FragmentBase {
args.putCharSequence("spanned", spanned);
}
args.putStringArrayList("groups", new ArrayList<>(db.answer().getGroups()));
return answer;
}
@ -274,6 +285,9 @@ public class FragmentAnswer extends FragmentBase {
etText.setText((Spanned) args.getCharSequence("spanned"));
}
adapterGroup.clear();
adapterGroup.addAll(args.getStringArrayList("groups"));
if (answer == null)
bottom_navigation.getMenu().removeItem(R.id.action_delete);

View File

@ -179,13 +179,13 @@ public class FragmentDialogAccount extends FragmentDialogBase {
if (swipes != null && swipes.size() == 1) {
String left;
if (swipes.get(0).swipe_left != null && swipes.get(0).swipe_left < 0)
left = getSwipeTitle(context, (long) swipes.get(0).swipe_left);
left = FragmentDialogSwipes.getActionTitle(context, swipes.get(0).swipe_left);
else
left = swipes.get(0).left_name;
String right;
if (swipes.get(0).swipe_right != null && swipes.get(0).swipe_right < 0)
right = getSwipeTitle(context, (long) swipes.get(0).swipe_right);
right = FragmentDialogSwipes.getActionTitle(context, swipes.get(0).swipe_right);
else
right = swipes.get(0).right_name;
@ -196,16 +196,6 @@ public class FragmentDialogAccount extends FragmentDialogBase {
tvRight.setText("?");
}
}
private String getSwipeTitle(Context context, long type) {
if (type == EntityMessage.SWIPE_ACTION_SEEN)
return context.getString(R.string.title_seen);
if (type == EntityMessage.SWIPE_ACTION_DELETE)
return context.getString(R.string.title_delete_permanently);
return "???";
}
});
db.folder().liveSystemFolders(account).observe(this, new Observer<List<EntityFolder>>() {

View File

@ -24,6 +24,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
@ -46,38 +47,27 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_swipes, null);
final Context context = getContext();
View dview = LayoutInflater.from(context).inflate(R.layout.dialog_swipes, null);
spLeft = dview.findViewById(R.id.spLeft);
spRight = dview.findViewById(R.id.spRight);
adapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList<EntityFolder>());
adapter = new ArrayAdapter<>(context, R.layout.spinner_item1, android.R.id.text1, new ArrayList<EntityFolder>());
adapter.setDropDownViewResource(R.layout.spinner_item1_dropdown);
spLeft.setAdapter(adapter);
spRight.setAdapter(adapter);
List<EntityFolder> folders = FragmentAccount.getFolderActions(getContext());
adapter.addAll(getFolderActions(context));
EntityFolder trash = new EntityFolder();
trash.id = 2L;
trash.name = getString(R.string.title_trash);
folders.add(1, trash);
EntityFolder archive = new EntityFolder();
archive.id = 1L;
archive.name = getString(R.string.title_archive);
folders.add(1, archive);
adapter.addAll(folders);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
int leftPos = prefs.getInt("swipe_left_default", 2); // Trash
int rightPos = prefs.getInt("swipe_right_default", 1); // Archive
spLeft.setSelection(leftPos);
spRight.setSelection(rightPos);
return new AlertDialog.Builder(getContext())
return new AlertDialog.Builder(context)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
@ -97,16 +87,9 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
.putBoolean("button_hide", true)
.apply();
Bundle args = new Bundle();
args.putLong("left", left == null ? 0 : left.id);
args.putLong("right", right == null ? 0 : right.id);
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) {
long left = args.getLong("left");
long right = args.getLong("right");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
@ -114,10 +97,7 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
List<EntityAccount> accounts = db.account().getAccounts();
for (EntityAccount account : accounts)
if (account.protocol == EntityAccount.TYPE_IMAP)
db.account().setAccountSwipes(
account.id,
getAction(context, left, account.id),
getAction(context, right, account.id));
setDefaultFolderActions(context, account.id);
db.setTransactionSuccessful();
} finally {
@ -129,30 +109,69 @@ public class FragmentDialogSwipes extends FragmentDialogBase {
@Override
protected void onExecuted(Bundle args, Void data) {
ToastEx.makeText(getContext(), R.string.title_completed, Toast.LENGTH_LONG).show();
ToastEx.makeText(context, R.string.title_completed, Toast.LENGTH_LONG).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
private Long getAction(Context context, long selection, long account) {
if (selection < 0)
return selection;
else if (selection == 0)
return null;
else {
DB db = DB.getInstance(context);
String type = (selection == 2 ? EntityFolder.TRASH : EntityFolder.ARCHIVE);
EntityFolder archive = db.folder().getFolderByType(account, type);
return (archive == null ? null : archive.id);
}
}
}.execute(getContext(), getViewLifecycleOwner(), args, "dialog:swipe");
}.execute(context, getViewLifecycleOwner(), new Bundle(), "dialog:swipe");
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
static void setDefaultFolderActions(Context context, long account) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
int leftPos = prefs.getInt("swipe_left_default", 2); // Trash
int rightPos = prefs.getInt("swipe_right_default", 1); // Archive
List<EntityFolder> actions = getFolderActions(context);
EntityFolder left = (leftPos < 0 || leftPos >= actions.size() ? null : actions.get(leftPos));
EntityFolder right = (rightPos < 0 || rightPos >= actions.size() ? null : actions.get(rightPos));
DB db = DB.getInstance(context);
db.account().setAccountSwipes(account,
getAction(context, left == null ? 0 : left.id, account),
getAction(context, right == null ? 0 : right.id, account));
}
static List<EntityFolder> getFolderActions(Context context) {
List<EntityFolder> folders = FragmentAccount.getFolderActions(context);
EntityFolder trash = new EntityFolder();
trash.id = 2L;
trash.name = context.getString(R.string.title_trash);
folders.add(1, trash);
EntityFolder archive = new EntityFolder();
archive.id = 1L;
archive.name = context.getString(R.string.title_archive);
folders.add(1, archive);
return folders;
}
static String getActionTitle(Context context, long id) {
for (EntityFolder action : getFolderActions(context))
if (action.id.equals(id))
return action.name;
return "???";
}
private static Long getAction(Context context, long selection, long account) {
if (selection < 0)
return selection;
else if (selection == 0)
return null;
else {
DB db = DB.getInstance(context);
String type = (selection == 2 ? EntityFolder.TRASH : EntityFolder.ARCHIVE);
EntityFolder folder = db.folder().getFolderByType(account, type);
return (folder == null ? null : folder.id);
}
}
}

View File

@ -557,13 +557,8 @@ public class FragmentGmail extends FragmentBase {
if (pop) {
account.swipe_left = EntityMessage.SWIPE_ACTION_DELETE;
account.swipe_right = EntityMessage.SWIPE_ACTION_SEEN;
} else {
for (EntityFolder folder : folders)
if (EntityFolder.TRASH.equals(folder.type))
account.swipe_left = folder.id;
else if (EntityFolder.ARCHIVE.equals(folder.type))
account.swipe_right = folder.id;
}
} else
FragmentDialogSwipes.setDefaultFolderActions(context, account.id);
db.account().updateAccount(account);

View File

@ -26,6 +26,8 @@ import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
import static android.text.format.DateUtils.FORMAT_SHOW_WEEKDAY;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.openintents.openpgp.OpenPgpSignatureResult.RESULT_KEY_MISSING;
import static org.openintents.openpgp.OpenPgpSignatureResult.RESULT_NO_SIGNATURE;
@ -2480,7 +2482,16 @@ public class FragmentMessages extends FragmentBase
}
@Override
public void setPosition(long id, Pair<Integer, Integer> position) {
public void setPosition(long id, Pair<Integer, Integer> delta, Pair<Integer, Integer> position) {
if (delta != null && delta.second != 0) {
boolean down = (delta.second > 0);
if (scrolling != down) {
scrolling = down;
updateCompose();
updateExpanded();
}
}
if (position == null)
positions.remove(id);
else
@ -2652,6 +2663,8 @@ public class FragmentMessages extends FragmentBase
@Override
public void layoutChanged() {
if (rvMessage == null)
return;
rvMessage.post(new Runnable() {
@Override
public void run() {
@ -4909,7 +4922,8 @@ public class FragmentMessages extends FragmentBase
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if ("pro".equals(key) || "banner_hidden".equals(key)) {
if (grpSupport != null &&
("pro".equals(key) || "banner_hidden".equals(key))) {
boolean pro = ActivityBilling.isPro(getContext());
long banner_hidden = prefs.getLong("banner_hidden", 0);
grpSupport.setVisibility(

View File

@ -943,13 +943,8 @@ public class FragmentOAuth extends FragmentBase {
if (pop) {
account.swipe_left = EntityMessage.SWIPE_ACTION_DELETE;
account.swipe_right = EntityMessage.SWIPE_ACTION_SEEN;
} else {
for (EntityFolder folder : folders)
if (EntityFolder.TRASH.equals(folder.type))
account.swipe_left = folder.id;
else if (EntityFolder.ARCHIVE.equals(folder.type))
account.swipe_right = folder.id;
}
} else
FragmentDialogSwipes.setDefaultFolderActions(context, account.id);
db.account().updateAccount(account);

View File

@ -33,6 +33,7 @@ import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@ -49,6 +50,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import com.google.android.material.textfield.TextInputLayout;
@ -78,8 +80,10 @@ public class FragmentQuickSetup extends FragmentBase {
private TextView tvPatience;
private TextView tvProgress;
private TextView tvArgument;
private TextView tvError;
private TextView tvErrorHint;
private Button btnManual;
private TextView tvInstructions;
private Button btnHelp;
private Button btnSupport;
@ -100,6 +104,7 @@ public class FragmentQuickSetup extends FragmentBase {
private Group grpSetup;
private Group grpCertificate;
private Group grpError;
private Group grpManual;
private int title;
private boolean update;
@ -146,8 +151,10 @@ public class FragmentQuickSetup extends FragmentBase {
tvPatience = view.findViewById(R.id.tvPatience);
tvProgress = view.findViewById(R.id.tvProgress);
tvArgument = view.findViewById(R.id.tvArgument);
tvError = view.findViewById(R.id.tvError);
tvErrorHint = view.findViewById(R.id.tvErrorHint);
btnManual = view.findViewById(R.id.btnManual);
tvInstructions = view.findViewById(R.id.tvInstructions);
btnHelp = view.findViewById(R.id.btnHelp);
btnSupport = view.findViewById(R.id.btnSupport);
@ -168,6 +175,7 @@ public class FragmentQuickSetup extends FragmentBase {
grpSetup = view.findViewById(R.id.grpSetup);
grpCertificate = view.findViewById(R.id.grpCertificate);
grpError = view.findViewById(R.id.grpError);
grpManual = view.findViewById(R.id.grpManual);
// Wire controls
@ -239,6 +247,19 @@ public class FragmentQuickSetup extends FragmentBase {
}
});
btnManual.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
FragmentAccount fragment = new FragmentAccount();
fragment.setArguments(new Bundle());
FragmentTransaction fragmentTransaction = getParentFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
fragmentTransaction.commit();
}
});
btnSupport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -254,6 +275,8 @@ public class FragmentQuickSetup extends FragmentBase {
tvPatience.setVisibility(View.GONE);
tvProgress.setVisibility(View.GONE);
pbSave.setVisibility(View.GONE);
tvArgument.setVisibility(View.GONE);
tvErrorHint.setVisibility(View.GONE);
tvInstructions.setVisibility(View.GONE);
tvInstructions.setMovementMethod(LinkMovementMethod.getInstance());
btnHelp.setVisibility(View.GONE);
@ -263,6 +286,7 @@ public class FragmentQuickSetup extends FragmentBase {
grpSetup.setVisibility(View.GONE);
grpCertificate.setVisibility(View.GONE);
grpError.setVisibility(View.GONE);
grpManual.setVisibility(View.GONE);
if (savedInstanceState != null) {
tilPassword.getEditText().setText(savedInstanceState.getString("fair:password"));
@ -275,10 +299,43 @@ public class FragmentQuickSetup extends FragmentBase {
}
private void onSave(boolean check) {
String name = etName.getText().toString().trim();
String email = etEmail.getText().toString().trim();
String password = tilPassword.getEditText().getText().toString();
String warning = null;
if (TextUtils.isEmpty(name))
warning = getString(R.string.title_no_name);
else if (TextUtils.isEmpty(email))
warning = getString(R.string.title_no_email);
else if (!Helper.EMAIL_ADDRESS.matcher(email).matches())
warning = getString(R.string.title_email_invalid, email);
else if (TextUtils.isEmpty(password))
warning = getString(R.string.title_no_password);
else {
ConnectivityManager cm = Helper.getSystemService(getContext(), ConnectivityManager.class);
NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
if (ani == null || !ani.isConnected())
warning = getString(R.string.title_no_internet);
}
if (warning != null) {
tvArgument.setText(warning);
tvArgument.setVisibility(View.VISIBLE);
getMainHandler().post(new Runnable() {
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
scroll.smoothScrollTo(0, tvArgument.getBottom());
}
});
return;
}
Bundle args = new Bundle();
args.putString("name", etName.getText().toString().trim());
args.putString("email", etEmail.getText().toString().trim());
args.putString("password", tilPassword.getEditText().getText().toString());
args.putString("name", name);
args.putString("email", email);
args.putString("password", password);
args.putBoolean("update", cbUpdate.isChecked());
args.putBoolean("check", check);
args.putParcelable("best", bestProvider);
@ -293,6 +350,9 @@ public class FragmentQuickSetup extends FragmentBase {
tvPatience.setVisibility(check ? View.VISIBLE : View.GONE);
pbSave.setVisibility(check ? View.GONE : View.VISIBLE);
grpError.setVisibility(View.GONE);
grpManual.setVisibility(View.GONE);
tvArgument.setVisibility(View.GONE);
tvErrorHint.setVisibility(View.GONE);
tvInstructions.setVisibility(View.GONE);
btnHelp.setVisibility(View.GONE);
cbUpdate.setVisibility(check ? View.GONE : View.VISIBLE);
@ -319,23 +379,9 @@ public class FragmentQuickSetup extends FragmentBase {
boolean check = args.getBoolean("check");
EmailProvider best = args.getParcelable("best");
if (TextUtils.isEmpty(name))
throw new IllegalArgumentException(context.getString(R.string.title_no_name));
if (TextUtils.isEmpty(email))
throw new IllegalArgumentException(context.getString(R.string.title_no_email));
if (!Helper.EMAIL_ADDRESS.matcher(email).matches())
throw new IllegalArgumentException(context.getString(R.string.title_email_invalid, email));
if (TextUtils.isEmpty(password))
throw new IllegalArgumentException(context.getString(R.string.title_no_password));
int at = email.indexOf('@');
String username = email.substring(0, at);
ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class);
NetworkInfo ani = (cm == null ? null : cm.getActiveNetworkInfo());
if (ani == null || !ani.isConnected())
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
Throwable fail = null;
List<EmailProvider> providers;
if (best == null)
@ -576,13 +622,7 @@ public class FragmentQuickSetup extends FragmentBase {
}
// Set swipe left/right folder
for (EntityFolder folder : folders)
if (EntityFolder.TRASH.equals(folder.type))
account.swipe_left = folder.id;
else if (EntityFolder.ARCHIVE.equals(folder.type))
account.swipe_right = folder.id;
db.account().updateAccount(account);
FragmentDialogSwipes.setDefaultFolderActions(context, account.id);
// Create identity
EntityIdentity identity = new EntityIdentity();
@ -679,8 +719,11 @@ public class FragmentQuickSetup extends FragmentBase {
if (provider != null && provider.appPassword)
message += "\n\n" + getString(R.string.title_setup_app_password_hint);
tvErrorHint.setText(message);
tvErrorHint.setVisibility(View.VISIBLE);
if (provider == null)
grpManual.setVisibility(View.VISIBLE);
} else
tvErrorHint.setText(R.string.title_setup_no_settings_hint);
grpManual.setVisibility(View.VISIBLE);
if (ex instanceof IllegalArgumentException || ex instanceof UnknownHostException) {
tvError.setText(ex.getMessage());

View File

@ -43,6 +43,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
@ -86,7 +87,7 @@ public class FragmentRule extends FragmentBase {
private TextView tvFolder;
private EditText etName;
private EditText etGroup;
private AutoCompleteTextView etGroup;
private EditText etOrder;
private CheckBox cbEnabled;
private CheckBox cbDaily;
@ -185,6 +186,7 @@ public class FragmentRule extends FragmentBase {
private Group grpDelete;
private Group grpLocalOnly;
private ArrayAdapter<String> adapterGroup;
private ArrayAdapter<String> adapterDay;
private ArrayAdapter<Action> adapterAction;
private ArrayAdapter<EntityIdentity> adapterIdentity;
@ -369,6 +371,10 @@ public class FragmentRule extends FragmentBase {
grpDelete = view.findViewById(R.id.grpDelete);
grpLocalOnly = view.findViewById(R.id.grpLocalOnly);
adapterGroup = new ArrayAdapter<>(getContext(), R.layout.spinner_item1_dropdown, android.R.id.text1);
etGroup.setThreshold(1);
etGroup.setAdapter(adapterGroup);
cbDaily.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -839,6 +845,7 @@ public class FragmentRule extends FragmentBase {
DB db = DB.getInstance(context);
data.account = db.account().getAccount(aid);
data.folder = db.folder().getFolder(fid);
data.groups = db.rule().getGroups();
data.identities = db.identity().getSynchronizingIdentities(aid);
data.answers = db.answer().getAnswers(false);
@ -851,6 +858,9 @@ public class FragmentRule extends FragmentBase {
data.account == null ? "" : data.account.name,
data.folder.getDisplayName(getContext())));
adapterGroup.clear();
adapterGroup.addAll(data.groups);
adapterIdentity.clear();
adapterIdentity.addAll(data.identities);
@ -1692,6 +1702,7 @@ public class FragmentRule extends FragmentBase {
private static class RefData {
EntityAccount account;
EntityFolder folder;
List<String> groups;
List<EntityIdentity> identities;
List<EntityAnswer> answers;
}

View File

@ -57,6 +57,7 @@ import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.Base64;
import android.util.Pair;
import android.util.Patterns;
import android.view.View;
@ -114,6 +115,7 @@ import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
@ -873,17 +875,8 @@ public class HtmlHelper {
break;
case "font-weight":
if (element.parent() != null) {
Integer fweight = getFontWeight(value);
if (fweight != null && fweight >= 600) {
Element strong = new Element("strong");
for (Node child : new ArrayList<>(element.childNodes())) {
child.remove();
strong.appendChild(child);
}
element.appendChild(strong);
}
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
sb.append(key).append(":").append(value).append(";");
break;
case "font-family":
@ -1889,6 +1882,8 @@ public class HtmlHelper {
return 300;
case "normal":
case "regular":
case "unset":
case "initial":
return 400;
case "bolder":
case "strong":
@ -1899,8 +1894,6 @@ public class HtmlHelper {
return 900;
case "none":
case "auto":
case "unset":
case "initial":
case "inherit":
return null;
}
@ -3326,6 +3319,11 @@ public class HtmlHelper {
Log.i(ex);
}
break;
case "font-weight":
Integer fweight = getFontWeight(value);
if (fweight != null)
setSpan(ssb, new StyleSpan(fweight >= 600 ? Typeface.BOLD : Typeface.NORMAL), start, ssb.length());
break;
case "font-family":
if ("wingdings".equalsIgnoreCase(value)) {
if (wingdings == null)
@ -3773,6 +3771,46 @@ public class HtmlHelper {
ssb.setSpan(spans[i], s, e, f);
}
for (Object bold : spans) {
if (bold instanceof StyleSpan) {
int style = ((StyleSpan) bold).getStyle();
if (style == Typeface.BOLD) {
int bs = start.get(bold);
int be = end.get(bold);
List<StyleSpan> normal = new ArrayList<>();
for (StyleSpan ss : ssb.getSpans(bs, be, StyleSpan.class))
if (ss.getStyle() == Typeface.NORMAL)
normal.add(ss);
if (normal.size() > 0) {
ssb.removeSpan(bold);
Collections.sort(normal, new Comparator<StyleSpan>() {
@Override
public int compare(StyleSpan s1, StyleSpan s2) {
int s = Integer.compare(ssb.getSpanStart(s1), ssb.getSpanStart(s2));
if (s != 0)
return s;
return -Integer.compare(ssb.getSpanEnd(s1), ssb.getSpanEnd(s2));
}
});
for (StyleSpan n : normal) {
int ns = start.get(n);
if (ns > bs) {
ssb.setSpan(new StyleSpan(Typeface.BOLD), bs, ns, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
bs = end.get(n);
}
}
if (bs < be)
ssb.setSpan(new StyleSpan(Typeface.BOLD), bs, be, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
return ssb;
}

View File

@ -3074,6 +3074,12 @@ public class Log {
ri.activityInfo.packageName, label, tabs, def));
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String open_with_pkg = prefs.getString("open_with_pkg", null);
boolean open_with_tabs = prefs.getBoolean("open_with_tabs", true);
size += write(os, String.format("Selected: %s tabs=%b\r\n",
open_with_pkg, open_with_tabs));
size += write(os, "\r\n");
} catch (Throwable ex) {
size += write(os, String.format("%s\r\n", ex));

View File

@ -2307,8 +2307,8 @@ public class MessageHelper {
if (records.length == 0)
return null;
Log.i("DKIM got " + records[0].name);
Map<String, String> dk = getKeyValues(records[0].name);
Log.i("DKIM got " + records[0].response);
Map<String, String> dk = getKeyValues(records[0].response);
String canonic = kv.get("c");
Log.i("DKIM canonicalization=" + canonic);

View File

@ -77,7 +77,9 @@ public class NoStreamException extends SecurityException {
builder.setView(dview);
builder.setNegativeButton(android.R.string.cancel, null);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
// https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
builder.setPositiveButton(R.string.title_setup_grant, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {

View File

@ -37,17 +37,21 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.mail.MessagingException;
public class ServiceExternal extends Service {
private static final String ACTION_POLL = BuildConfig.APPLICATION_ID + ".POLL";
private static final String ACTION_ENABLE = BuildConfig.APPLICATION_ID + ".ENABLE";
private static final String ACTION_DISABLE = BuildConfig.APPLICATION_ID + ".DISABLE";
private static final String ACTION_INTERVAL = BuildConfig.APPLICATION_ID + ".INTERVAL";
private static final String ACTION_RULE = BuildConfig.APPLICATION_ID + ".RULE";
private static final String ACTION_DISCONNECT_ME = BuildConfig.APPLICATION_ID + ".DISCONNECT.ME";
// adb shell am start-foreground-service -a eu.faircode.email.POLL --es account Gmail
// adb shell am start-foreground-service -a eu.faircode.email.ENABLE --es account Gmail
// adb shell am start-foreground-service -a eu.faircode.email.DISABLE --es account Gmail
// adb shell am start-foreground-service -a eu.faircode.email.INTERVAL --ei minutes {0, 15, 30, 60, 120, 240, 480, 1440}
// adb shell am start-foreground-service -a eu.faircode.email.RULE --es account Gmail -e rule Test
// adb shell am start-foreground-service -a eu.faircode.email.DISCONNECT
@Override
@ -96,6 +100,9 @@ public class ServiceExternal extends Service {
case ACTION_INTERVAL:
interval(context, intent);
break;
case ACTION_RULE:
rule(context, intent);
break;
case ACTION_DISCONNECT_ME:
disconnect(context, intent);
break;
@ -196,6 +203,53 @@ public class ServiceExternal extends Service {
ServiceSynchronize.eval(context, "external account=" + accountName + " enabled=" + enabled);
}
private static void rule(Context context, Intent intent) throws IOException, JSONException, MessagingException {
String accountName = intent.getStringExtra("account");
String ruleName = intent.getStringExtra("rule");
DB db = DB.getInstance(context);
EntityAccount account = db.account().getAccount(accountName);
if (account == null)
throw new IllegalArgumentException("Account not found name=" + accountName);
List<EntityRule> rules = db.rule().getRuleByName(account.id, ruleName);
if (rules == null || rules.size() == 0)
throw new IllegalArgumentException("Rule not found name=" + ruleName);
if (rules.size() != 1)
throw new IllegalArgumentException("Rule ambiguous name=" + ruleName);
EntityRule rule = rules.get(0);
List<Long> ids = db.message().getMessageIdsByFolder(rule.folder);
if (ids == null || ids.size() == 0)
return;
// Check header conditions
for (long mid : ids) {
EntityMessage message = db.message().getMessage(mid);
if (message == null || message.ui_hide)
continue;
rule.matches(context, message, null, null);
}
int applied = 0;
for (long mid : ids)
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(mid);
if (message == null || message.ui_hide)
continue;
EntityLog.log(context, "Executing rules message=" + message.id);
applied = EntityRule.run(context, rules, message, null, null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
EntityLog.log(context, "Executing rule=" + rule.name + " applied=" + applied);
}
private static void disconnect(Context context, Intent intent) throws IOException, JSONException {
DisconnectBlacklist.download(context);
}

View File

@ -90,7 +90,7 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
}
void init(int height, int maxHeight, float size, Pair<Integer, Integer> position, boolean force_light, IWebView intf) {
Log.i("Init height=" + height + "/" + maxHeight + " size=" + size);
Log.i("Init height=" + height + "/" + maxHeight + " size=" + size + " accelerated=" + isHardwareAccelerated());
if (maxHeight == 0) {
Log.e("WebView max height zero");
@ -188,7 +188,7 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
Log.i("Scroll (x,y)=" + scrollX + "," + scrollY);
intf.onScrollChange(scrollX, scrollY);
intf.onScrollChange(scrollX - oldScrollX, scrollY - oldScrollY, scrollX, scrollY);
}
});
}
@ -316,18 +316,20 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
intercept = (yoff > 0 || dy >= 0) && (yoff < bottom || dy <= 0);
}
int xrange = computeHorizontalScrollRange();
int xextend = computeHorizontalScrollExtent();
boolean canScrollHorizontal = (xrange > xextend);
if (canScrollHorizontal) {
int right = xrange - xextend;
int xoff = computeHorizontalScrollOffset();
int ldx = xoff - lastXoff;
float dx = lastX - event.getX();
intercept = (xoff > 0 || dx >= 0) &&
(xoff < right || dx <= 0) &&
(Math.signum(dx) == Math.signum(ldx));
lastXoff = xoff;
if (!intercept) {
int xrange = computeHorizontalScrollRange();
int xextend = computeHorizontalScrollExtent();
boolean canScrollHorizontal = (xrange > xextend);
if (canScrollHorizontal) {
int right = xrange - xextend;
int xoff = computeHorizontalScrollOffset();
int ldx = xoff - lastXoff;
float dx = lastX - event.getX();
intercept = (xoff > 0 || dx >= 0) &&
(xoff < right || dx <= 0) &&
(Math.signum(dx) == Math.signum(ldx));
lastXoff = xoff;
}
}
}
getParent().requestDisallowInterceptTouchEvent(intercept || event.getPointerCount() > 1);
@ -410,7 +412,7 @@ public class WebViewEx extends WebView implements DownloadListener, View.OnLongC
void onScaleChanged(float newScale);
void onScrollChange(int scrollX, int scrollY);
void onScrollChange(int dx, int dy, int scrollX, int scrollY);
boolean onOpenLink(String url);
}

View File

@ -52,7 +52,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etName" />
<eu.faircode.email.EditTextPlain
<eu.faircode.email.EditTextAutoComplete
android:id="@+id/etGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@ -153,6 +153,19 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPatience" />
<TextView
android:id="@+id/tvArgument"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:autoLink="web"
android:text="Argument"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?attr/colorWarning"
android:textIsSelectable="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvProgress" />
<TextView
android:id="@+id/tvErrorTitle"
android:layout_width="wrap_content"
@ -162,7 +175,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvProgress" />
app:layout_constraintTop_toBottomOf="@id/tvArgument" />
<TextView
android:id="@+id/tvError"
@ -189,17 +202,39 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvError" />
<TextView
android:id="@+id/tvManualHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_setup_no_settings_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvErrorHint" />
<Button
android:id="@+id/btnManual"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/twotone_edit_24"
android:drawablePadding="6dp"
android:text="@string/title_setup_manual_setup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvManualHint" />
<TextView
android:id="@+id/tvErrorRemark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginTop="12dp"
android:text="@string/title_setup_quick_support"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvErrorHint" />
app:layout_constraintTop_toBottomOf="@id/btnManual" />
<TextView
android:id="@+id/tvInstructions"
@ -416,6 +451,12 @@
android:id="@+id/grpError"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvErrorTitle,tvError,tvErrorHint,tvErrorRemark,btnSupport" />
app:constraint_referenced_ids="tvErrorTitle,tvError,tvErrorRemark,btnSupport" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpManual"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvManualHint,btnManual" />
</androidx.constraintlayout.widget.ConstraintLayout>
</eu.faircode.email.ScrollViewEx>

View File

@ -81,7 +81,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
<eu.faircode.email.EditTextPlain
<eu.faircode.email.EditTextAutoComplete
android:id="@+id/etGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@ -283,6 +283,7 @@
<string name="title_setup_done">Done</string>
<string name="title_setup_still">Still to do</string>
<string name="title_setup_error">Error</string>
<string name="title_setup_manual_setup">Manual setup</string>
<string name="title_setup_pop">Use POP3 (not recommended)</string>
<string name="title_setup_recent">Use \'recent\' for multiple email clients</string>
<string name="title_setup_configuring">Configuring account &#8230;</string>
@ -1866,7 +1867,7 @@
<string name="title_rule_daily">Run daily (only)</string>
<string name="title_rule_age">Messages older than (days)</string>
<string name="title_rule_stop">Stop processing rules after executing this rule</string>
<string name="title_rule_stop_remark">If the rule is part of a group, only the processing of the group will be stopped</string>
<string name="title_rule_stop_remark">If the rule is part of a group, the processing of all rules of this group will be stopped</string>
<string name="title_rule_sender">Sender contains</string>
<string name="title_rule_sender_known">Sender is a contact</string>
<string name="title_rule_recipient">Recipient contains</string>

View File

@ -2050,4 +2050,18 @@
port="465"
starttls="false" />
</provider>
<provider
name="FREE!"
appPassword="true"
domain="free\\.de"
link="https://faq.free.de/content/310251/44/de/wie-lauten-die-einstellungen-fuer-den-zugriff-auf-mein-free-@freede-mailkonto.html">
<imap
host="pop.free.de"
port="993"
starttls="false" />
<smtp
host="smtp.free.de"
port="465"
starttls="false" />
</provider>
</providers>

View File

@ -561,6 +561,7 @@
<action android:name="${applicationId}.ENABLE" />
<action android:name="${applicationId}.DISABLE" />
<action android:name="${applicationId}.INTERVAL" />
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
</intent-filter>
</service>

File diff suppressed because it is too large Load Diff