mirror of
https://github.com/M66B/FairEmail.git
synced 2025-03-19 02:15:28 +00:00
Show message signers
This commit is contained in:
parent
bf91c80b7e
commit
1bd7b4cfdb
10 changed files with 3195 additions and 175 deletions
2903
app/schemas/eu.faircode.email.DB/269.json
Normal file
2903
app/schemas/eu.faircode.email.DB/269.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -272,6 +272,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
private boolean avatars;
|
||||
private boolean color_stripe;
|
||||
private boolean check_authentication;
|
||||
private boolean native_dkim;
|
||||
private boolean check_tls;
|
||||
private boolean check_reply_domain;
|
||||
private boolean check_mx;
|
||||
|
@ -400,6 +401,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
private ImageButton ibPinContact;
|
||||
private ImageButton ibAddContact;
|
||||
|
||||
private TextView tvSignedByTitle;
|
||||
private TextView tvSubmitterTitle;
|
||||
private TextView tvDeliveredToTitle;
|
||||
private TextView tvFromExTitle;
|
||||
|
@ -415,6 +417,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
private TextView tvLanguageTitle;
|
||||
private TextView tvThreadTitle;
|
||||
|
||||
private TextView tvSignedBy;
|
||||
private TextView tvSubmitter;
|
||||
private TextView tvDeliveredTo;
|
||||
private TextView tvFromEx;
|
||||
|
@ -806,6 +809,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibPinContact = vsBody.findViewById(R.id.ibPinContact);
|
||||
ibAddContact = vsBody.findViewById(R.id.ibAddContact);
|
||||
|
||||
tvSignedByTitle = vsBody.findViewById(R.id.tvSignedByTitle);
|
||||
tvSubmitterTitle = vsBody.findViewById(R.id.tvSubmitterTitle);
|
||||
tvDeliveredToTitle = vsBody.findViewById(R.id.tvDeliveredToTitle);
|
||||
tvFromExTitle = vsBody.findViewById(R.id.tvFromExTitle);
|
||||
|
@ -821,6 +825,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
tvLanguageTitle = vsBody.findViewById(R.id.tvLanguageTitle);
|
||||
tvThreadTitle = vsBody.findViewById(R.id.tvThreadTitle);
|
||||
|
||||
tvSignedBy = vsBody.findViewById(R.id.tvSignedBy);
|
||||
tvSubmitter = vsBody.findViewById(R.id.tvSubmitter);
|
||||
tvDeliveredTo = vsBody.findViewById(R.id.tvDeliveredTo);
|
||||
tvFromEx = vsBody.findViewById(R.id.tvFromEx);
|
||||
|
@ -1680,6 +1685,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibPinContact.setVisibility(View.GONE);
|
||||
ibAddContact.setVisibility(View.GONE);
|
||||
|
||||
tvSignedByTitle.setVisibility(View.GONE);
|
||||
tvSubmitterTitle.setVisibility(View.GONE);
|
||||
tvDeliveredToTitle.setVisibility(View.GONE);
|
||||
tvFromExTitle.setVisibility(View.GONE);
|
||||
|
@ -1695,6 +1701,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
tvLanguageTitle.setVisibility(View.GONE);
|
||||
tvThreadTitle.setVisibility(View.GONE);
|
||||
|
||||
tvSignedBy.setVisibility(View.GONE);
|
||||
tvSubmitter.setVisibility(View.GONE);
|
||||
tvDeliveredTo.setVisibility(View.GONE);
|
||||
tvFromEx.setVisibility(View.GONE);
|
||||
|
@ -2486,6 +2493,29 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
ibPinContact.setVisibility(show_addresses && pin && contacts && froms > 0 ? View.VISIBLE : View.GONE);
|
||||
ibAddContact.setVisibility(show_addresses && contacts && froms > 0 ? View.VISIBLE : View.GONE);
|
||||
|
||||
|
||||
boolean known_signer = false;
|
||||
if (native_dkim &&
|
||||
message.signedby != null &&
|
||||
message.from != null &&
|
||||
message.from.length == 1) {
|
||||
String domain = UriHelper.getEmailDomain(((InternetAddress) message.from[0]).getAddress());
|
||||
if (domain != null)
|
||||
for (String signer : message.signedby.split(","))
|
||||
if (signer.equals(domain)) {
|
||||
known_signer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean show_signers = (native_dkim &&
|
||||
message.signedby != null &&
|
||||
(show_addresses || !known_signer));
|
||||
|
||||
tvSignedByTitle.setVisibility(show_signers ? View.VISIBLE : View.GONE);
|
||||
tvSignedBy.setVisibility(show_signers ? View.VISIBLE : View.GONE);
|
||||
tvSignedBy.setTextColor(known_signer ? textColorTertiary : colorAccent);
|
||||
tvSignedBy.setText(message.signedby);
|
||||
|
||||
tvSubmitterTitle.setVisibility(!TextUtils.isEmpty(submitter) ? View.VISIBLE : View.GONE);
|
||||
tvSubmitter.setVisibility(!TextUtils.isEmpty(submitter) ? View.VISIBLE : View.GONE);
|
||||
tvSubmitter.setText(submitter);
|
||||
|
@ -4578,6 +4608,14 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
.append(message.dmarc == null ? "-" : (message.dmarc ? "✓" : "✗"));
|
||||
}
|
||||
|
||||
if (native_dkim && !TextUtils.isEmpty(message.signedby)) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append("Signed by:").append('\n');
|
||||
for (String signer : message.signedby.split(","))
|
||||
sb.append(signer).append('\n');
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(message.blocklist)) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
|
@ -7402,6 +7440,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
this.avatars = (contacts && avatars) || (bimi || gravatars || libravatars || favicons || generated);
|
||||
this.color_stripe = prefs.getBoolean("color_stripe", true);
|
||||
this.check_authentication = prefs.getBoolean("check_authentication", true);
|
||||
this.native_dkim = prefs.getBoolean("native_dkim", false);
|
||||
this.check_tls = prefs.getBoolean("check_tls", true);
|
||||
this.check_reply_domain = prefs.getBoolean("check_reply_domain", true);
|
||||
this.check_mx = prefs.getBoolean("check_mx", false);
|
||||
|
@ -7532,6 +7571,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
same = false;
|
||||
log("bimi_selector changed", next.id);
|
||||
}
|
||||
if (!Objects.equals(prev.signedby, next.signedby)) {
|
||||
same = false;
|
||||
log("signedby changed", next.id);
|
||||
}
|
||||
if (!Objects.equals(prev.tls, next.tls)) {
|
||||
same = false;
|
||||
log("tls changed", next.id);
|
||||
|
|
|
@ -4076,6 +4076,7 @@ class Core {
|
|||
boolean download_headers = prefs.getBoolean("download_headers", false);
|
||||
boolean download_plain = prefs.getBoolean("download_plain", false);
|
||||
boolean notify_known = prefs.getBoolean("notify_known", false);
|
||||
boolean native_dkim = prefs.getBoolean("native_dkim", false);
|
||||
boolean experiments = prefs.getBoolean("experiments", false);
|
||||
boolean pro = ActivityBilling.isPro(context);
|
||||
|
||||
|
@ -4255,15 +4256,14 @@ class Core {
|
|||
message.receipt_request = helper.getReceiptRequested();
|
||||
message.receipt_to = helper.getReceiptTo();
|
||||
message.bimi_selector = helper.getBimiSelector();
|
||||
|
||||
if (native_dkim) {
|
||||
List<String> signers = helper.verifyDKIM(context);
|
||||
message.signedby = (signers.size() == 0 ? null : TextUtils.join(",", signers));
|
||||
}
|
||||
|
||||
message.tls = helper.getTLS();
|
||||
message.dkim = MessageHelper.getAuthentication("dkim", authentication);
|
||||
if (BuildConfig.DEBUG &&
|
||||
Boolean.TRUE.equals(message.dkim) &&
|
||||
EntityFolder.JUNK.equals(folder.type)) {
|
||||
Boolean dkim = helper.verifyDKIM(context);
|
||||
flagged = Boolean.FALSE.equals(dkim);
|
||||
color = android.graphics.Color.RED;
|
||||
}
|
||||
if (Boolean.TRUE.equals(message.dkim))
|
||||
message.dkim = helper.checkDKIMRequirements();
|
||||
message.spf = MessageHelper.getAuthentication("spf", authentication);
|
||||
|
|
|
@ -68,7 +68,7 @@ import javax.mail.internet.InternetAddress;
|
|||
// https://developer.android.com/topic/libraries/architecture/room.html
|
||||
|
||||
@Database(
|
||||
version = 268,
|
||||
version = 269,
|
||||
entities = {
|
||||
EntityIdentity.class,
|
||||
EntityAccount.class,
|
||||
|
@ -2741,6 +2741,12 @@ public abstract class DB extends RoomDatabase {
|
|||
logMigration(startVersion, endVersion);
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `count_unread` INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
}).addMigrations(new Migration(268, 269) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
logMigration(startVersion, endVersion);
|
||||
db.execSQL("ALTER TABLE `message` ADD COLUMN `signedby` TEXT");
|
||||
}
|
||||
}).addMigrations(new Migration(998, 999) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
|
|
|
@ -155,6 +155,7 @@ public class EntityMessage implements Serializable {
|
|||
public Boolean receipt_request;
|
||||
public Address[] receipt_to;
|
||||
public String bimi_selector;
|
||||
public String signedby;
|
||||
public Boolean tls;
|
||||
public Boolean dkim;
|
||||
public Boolean spf;
|
||||
|
|
|
@ -217,6 +217,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
private SeekBar sbMaxBackOff;
|
||||
private SwitchCompat swLogarithmicBackoff;
|
||||
private SwitchCompat swExactAlarms;
|
||||
private SwitchCompat swNativeDkim;
|
||||
private SwitchCompat swInfra;
|
||||
private SwitchCompat swDupMsgId;
|
||||
private EditText etKeywords;
|
||||
|
@ -275,7 +276,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
"auth_plain", "auth_login", "auth_ntlm", "auth_sasl", "auth_apop", "use_top",
|
||||
"keep_alive_poll", "empty_pool", "idle_done", "fast_fetch",
|
||||
"max_backoff_power", "logarithmic_backoff",
|
||||
"exact_alarms", "infra", "dkim_verify", "dup_msgids", "global_keywords", "test_iab"
|
||||
"exact_alarms", "native_dkim", "infra", "dkim_verify", "dup_msgids", "global_keywords", "test_iab"
|
||||
};
|
||||
|
||||
private final static String[] RESET_QUESTIONS = new String[]{
|
||||
|
@ -440,6 +441,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
sbMaxBackOff = view.findViewById(R.id.sbMaxBackOff);
|
||||
swLogarithmicBackoff = view.findViewById(R.id.swLogarithmicBackoff);
|
||||
swExactAlarms = view.findViewById(R.id.swExactAlarms);
|
||||
swNativeDkim = view.findViewById(R.id.swNativeDkim);
|
||||
swInfra = view.findViewById(R.id.swInfra);
|
||||
swDupMsgId = view.findViewById(R.id.swDupMsgId);
|
||||
etKeywords = view.findViewById(R.id.etKeywords);
|
||||
|
@ -1585,6 +1587,13 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
}
|
||||
});
|
||||
|
||||
swNativeDkim.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
prefs.edit().putBoolean("native_dkim", checked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
swInfra.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
|
@ -2293,6 +2302,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
|
||||
swLogarithmicBackoff.setChecked(prefs.getBoolean("logarithmic_backoff", true));
|
||||
swExactAlarms.setChecked(prefs.getBoolean("exact_alarms", true));
|
||||
swNativeDkim.setChecked(prefs.getBoolean("native_dkim", false));
|
||||
swInfra.setChecked(prefs.getBoolean("infra", false));
|
||||
swDupMsgId.setChecked(prefs.getBoolean("dup_msgids", false));
|
||||
etKeywords.setText(prefs.getString("global_keywords", null));
|
||||
|
|
|
@ -1989,177 +1989,182 @@ public class MessageHelper {
|
|||
return true;
|
||||
}
|
||||
|
||||
Boolean verifyDKIM(Context context) throws MessagingException, IOException {
|
||||
if (!(imessage instanceof IMAPMessage))
|
||||
return null;
|
||||
@NonNull
|
||||
List<String> verifyDKIM(Context context) {
|
||||
List<String> signers = new ArrayList<>();
|
||||
|
||||
// Workaround reformatted headers
|
||||
Properties props = MessageHelper.getSessionProperties(true);
|
||||
Session isession = Session.getInstance(props, null);
|
||||
MimeMessage amessage = new MimeMessage(isession, ((IMAPMessage) imessage).getMimeStream());
|
||||
try {
|
||||
// Workaround reformatted headers
|
||||
Properties props = MessageHelper.getSessionProperties(true);
|
||||
Session isession = Session.getInstance(props, null);
|
||||
MimeMessage amessage = new MimeMessage(isession, ((IMAPMessage) imessage).getMimeStream());
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376/
|
||||
String[] headers = amessage.getHeader("DKIM-Signature");
|
||||
if (headers == null || headers.length < 1)
|
||||
return null;
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376/
|
||||
String[] headers = amessage.getHeader("DKIM-Signature");
|
||||
if (headers == null || headers.length < 1)
|
||||
return signers;
|
||||
|
||||
boolean valid = false;
|
||||
for (String header : headers) {
|
||||
Map<String, String> kv = getKeyValues(MimeUtility.unfold(header));
|
||||
for (String header : headers) {
|
||||
Map<String, String> kv = getKeyValues(MimeUtility.unfold(header));
|
||||
|
||||
String a = kv.get("a");
|
||||
String halgo;
|
||||
String salgo;
|
||||
if ("rsa-sha1".equals(a)) {
|
||||
halgo = "SHA-1";
|
||||
salgo = "SHA1withRSA";
|
||||
} else if ("rsa-sha256".equals(a)) {
|
||||
halgo = "SHA-256";
|
||||
salgo = "SHA256withRSA";
|
||||
} else {
|
||||
Log.i("DKIM a=" + a);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String dns = kv.get("s") + "._domainkey." + kv.get("d");
|
||||
Log.i("DKIM lookup " + dns);
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns, "txt");
|
||||
if (records.length > 0) {
|
||||
Log.i("DKIM got " + records[0].name);
|
||||
Map<String, String> dk = getKeyValues(records[0].name);
|
||||
|
||||
Log.i("DKIM canonicalization=" + kv.get("c"));
|
||||
String[] c = kv.get("c").split("/");
|
||||
|
||||
StringBuilder head = new StringBuilder();
|
||||
|
||||
String hs = kv.get("h");
|
||||
Log.i("DKIM headers=" + hs);
|
||||
|
||||
boolean from = false;
|
||||
List<String> _h = new ArrayList<>();
|
||||
for (String key : hs.split(":")) {
|
||||
_h.add(key.trim());
|
||||
from = (from || "from".equalsIgnoreCase(key.trim()));
|
||||
}
|
||||
if (!from)
|
||||
throw new IllegalArgumentException("from missing: " + hs);
|
||||
|
||||
_h.add("DKIM-Signature");
|
||||
|
||||
Map<String, Integer> index = new Hashtable<>();
|
||||
for (String n : _h) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4.2
|
||||
String _n = n.toLowerCase(Locale.ROOT);
|
||||
Integer idx = index.get(_n);
|
||||
idx = (idx == null ? 1 : idx + 1);
|
||||
index.put(_n, idx);
|
||||
|
||||
String[] h = ("DKIM-Signature".equals(n) ? new String[]{header} : amessage.getHeader(n));
|
||||
if (h == null || idx > h.length) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4
|
||||
Log.i("DKIM missing header=" + n + "[" + idx + "/" + (h == null ? null : h.length) + "]");
|
||||
continue;
|
||||
}
|
||||
|
||||
String v = h[h.length - idx];
|
||||
if ("DKIM-Signature".equals(n)) {
|
||||
int b = v.lastIndexOf("b=");
|
||||
int s = v.indexOf(";", b + 2);
|
||||
v = v.substring(0, b + 2) + (s < 0 ? "" : v.substring(s));
|
||||
} else
|
||||
Log.i("DKIM " + n + "=" + v.replaceAll("\\r?\\n", "|"));
|
||||
|
||||
if ("simple".equals(c[0])) {
|
||||
if ("DKIM-Signature".equals(n))
|
||||
head.append(n).append(": ").append(v);
|
||||
else {
|
||||
// Find original header/name
|
||||
Enumeration<Header> oheaders = amessage.getAllHeaders();
|
||||
while (oheaders.hasMoreElements()) {
|
||||
Header oheader = oheaders.nextElement();
|
||||
if (n.equalsIgnoreCase(oheader.getName()))
|
||||
head.append(oheader.getName()).append(": ")
|
||||
.append(oheader.getValue());
|
||||
}
|
||||
}
|
||||
} else if ("relaxed".equals(c[0])) {
|
||||
v = MimeUtility.unfold(v);
|
||||
head.append(_n).append(':')
|
||||
.append(v.replaceAll("\\s+", " ").trim());
|
||||
} else
|
||||
throw new IllegalArgumentException(c[0]);
|
||||
|
||||
if (!"DKIM-Signature".equals(n))
|
||||
head.append("\r\n");
|
||||
}
|
||||
Log.i("DKIM head=" + head.toString().replace("\r\n", "|"));
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
Helper.copy(amessage.getRawInputStream(), bos);
|
||||
String body = bos.toString(); // TODO: charset
|
||||
if ("simple".equals(c[c.length > 1 ? 1 : 0])) {
|
||||
if (TextUtils.isEmpty(body))
|
||||
body = "\r\n";
|
||||
else if (!body.endsWith("\r\n"))
|
||||
body += "\r\n";
|
||||
else {
|
||||
while (body.endsWith("\r\n\r\n"))
|
||||
body = body.substring(0, body.length() - 2);
|
||||
}
|
||||
} else if ("relaxed".equals(c[c.length > 1 ? 1 : 0])) {
|
||||
if (TextUtils.isEmpty(body))
|
||||
body = "";
|
||||
else {
|
||||
body = body.replaceAll("[ \\t]+\r\n", "\r\n");
|
||||
body = body.replaceAll("[ \\t]+", " ");
|
||||
while (body.endsWith("\r\n\r\n"))
|
||||
body = body.substring(0, body.length() - 2);
|
||||
if ("\r\n".equals(body))
|
||||
body = "";
|
||||
}
|
||||
} else
|
||||
throw new IllegalArgumentException(c[1]);
|
||||
|
||||
Log.i("DKIM body=" + body.replace("\r\n", "|"));
|
||||
|
||||
byte[] bh = MessageDigest.getInstance(halgo).digest(body.getBytes()); // TODO: charset
|
||||
Log.i("DKIM bh=" + Base64.encodeToString(bh, Base64.NO_WRAP) + "/" + kv.get("bh"));
|
||||
|
||||
String p = dk.get("p").replaceAll("\\s+", "");
|
||||
Log.i("DKIM pubkey=" + p);
|
||||
|
||||
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decode(p, Base64.DEFAULT));
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
|
||||
Signature sig = Signature.getInstance(salgo); // a=
|
||||
|
||||
String s = kv.get("b").replaceAll("\\s+", "");
|
||||
Log.i("DKIM signature=" + s);
|
||||
|
||||
byte[] signature = Base64.decode(s, Base64.DEFAULT);
|
||||
// TODO: check signature length
|
||||
|
||||
sig.initVerify(pubKey);
|
||||
sig.update(head.toString().getBytes());
|
||||
|
||||
boolean verified = sig.verify(signature);
|
||||
Log.i("DKIM valid=" + verified +
|
||||
" dns=" + dns +
|
||||
" from=" + formatAddresses(getFrom()));
|
||||
|
||||
if (verified)
|
||||
valid = true;
|
||||
String a = kv.get("a");
|
||||
String halgo;
|
||||
String salgo;
|
||||
if ("rsa-sha1".equals(a)) {
|
||||
halgo = "SHA-1";
|
||||
salgo = "SHA1withRSA";
|
||||
} else if ("rsa-sha256".equals(a)) {
|
||||
halgo = "SHA-256";
|
||||
salgo = "SHA256withRSA";
|
||||
} else {
|
||||
Log.i("DKIM a=" + a);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
String signer = kv.get("d");
|
||||
String dns = kv.get("s") + "._domainkey." + signer;
|
||||
Log.i("DKIM lookup " + dns);
|
||||
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns, "txt");
|
||||
if (records.length > 0) {
|
||||
Log.i("DKIM got " + records[0].name);
|
||||
Map<String, String> dk = getKeyValues(records[0].name);
|
||||
|
||||
Log.i("DKIM canonicalization=" + kv.get("c"));
|
||||
String[] c = kv.get("c").split("/");
|
||||
|
||||
StringBuilder head = new StringBuilder();
|
||||
|
||||
String hs = kv.get("h");
|
||||
Log.i("DKIM headers=" + hs);
|
||||
|
||||
boolean from = false;
|
||||
List<String> _h = new ArrayList<>();
|
||||
for (String key : hs.split(":")) {
|
||||
_h.add(key.trim());
|
||||
from = (from || "from".equalsIgnoreCase(key.trim()));
|
||||
}
|
||||
if (!from)
|
||||
throw new IllegalArgumentException("from missing: " + hs);
|
||||
|
||||
_h.add("DKIM-Signature");
|
||||
|
||||
Map<String, Integer> index = new Hashtable<>();
|
||||
for (String n : _h) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4.2
|
||||
String _n = n.toLowerCase(Locale.ROOT);
|
||||
Integer idx = index.get(_n);
|
||||
idx = (idx == null ? 1 : idx + 1);
|
||||
index.put(_n, idx);
|
||||
|
||||
String[] h = ("DKIM-Signature".equals(n) ? new String[]{header} : amessage.getHeader(n));
|
||||
if (h == null || idx > h.length) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4
|
||||
Log.i("DKIM missing header=" + n + "[" + idx + "/" + (h == null ? null : h.length) + "]");
|
||||
continue;
|
||||
}
|
||||
|
||||
String v = h[h.length - idx];
|
||||
if ("DKIM-Signature".equals(n)) {
|
||||
int b = v.lastIndexOf("b=");
|
||||
int s = v.indexOf(";", b + 2);
|
||||
v = v.substring(0, b + 2) + (s < 0 ? "" : v.substring(s));
|
||||
} else
|
||||
Log.i("DKIM " + n + "=" + v.replaceAll("\\r?\\n", "|"));
|
||||
|
||||
if ("simple".equals(c[0])) {
|
||||
if ("DKIM-Signature".equals(n))
|
||||
head.append(n).append(": ").append(v);
|
||||
else {
|
||||
// Find original header/name
|
||||
Enumeration<Header> oheaders = amessage.getAllHeaders();
|
||||
while (oheaders.hasMoreElements()) {
|
||||
Header oheader = oheaders.nextElement();
|
||||
if (n.equalsIgnoreCase(oheader.getName()))
|
||||
head.append(oheader.getName()).append(": ")
|
||||
.append(oheader.getValue());
|
||||
}
|
||||
}
|
||||
} else if ("relaxed".equals(c[0])) {
|
||||
v = MimeUtility.unfold(v);
|
||||
head.append(_n).append(':')
|
||||
.append(v.replaceAll("\\s+", " ").trim());
|
||||
} else
|
||||
throw new IllegalArgumentException(c[0]);
|
||||
|
||||
if (!"DKIM-Signature".equals(n))
|
||||
head.append("\r\n");
|
||||
}
|
||||
Log.i("DKIM head=" + head.toString().replace("\r\n", "|"));
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
Helper.copy(amessage.getRawInputStream(), bos);
|
||||
String body = bos.toString(); // TODO: charset
|
||||
if ("simple".equals(c[c.length > 1 ? 1 : 0])) {
|
||||
if (TextUtils.isEmpty(body))
|
||||
body = "\r\n";
|
||||
else if (!body.endsWith("\r\n"))
|
||||
body += "\r\n";
|
||||
else {
|
||||
while (body.endsWith("\r\n\r\n"))
|
||||
body = body.substring(0, body.length() - 2);
|
||||
}
|
||||
} else if ("relaxed".equals(c[c.length > 1 ? 1 : 0])) {
|
||||
if (TextUtils.isEmpty(body))
|
||||
body = "";
|
||||
else {
|
||||
body = body.replaceAll("[ \\t]+\r\n", "\r\n");
|
||||
body = body.replaceAll("[ \\t]+", " ");
|
||||
while (body.endsWith("\r\n\r\n"))
|
||||
body = body.substring(0, body.length() - 2);
|
||||
if ("\r\n".equals(body))
|
||||
body = "";
|
||||
}
|
||||
} else
|
||||
throw new IllegalArgumentException(c[1]);
|
||||
|
||||
Log.i("DKIM body=" + body.replace("\r\n", "|"));
|
||||
|
||||
byte[] bh = MessageDigest.getInstance(halgo).digest(body.getBytes()); // TODO: charset
|
||||
Log.i("DKIM bh=" + Base64.encodeToString(bh, Base64.NO_WRAP) + "/" + kv.get("bh"));
|
||||
|
||||
String p = dk.get("p").replaceAll("\\s+", "");
|
||||
Log.i("DKIM pubkey=" + p);
|
||||
|
||||
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decode(p, Base64.DEFAULT));
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
|
||||
Signature sig = Signature.getInstance(salgo); // a=
|
||||
|
||||
String s = kv.get("b").replaceAll("\\s+", "");
|
||||
Log.i("DKIM signature=" + s);
|
||||
|
||||
byte[] signature = Base64.decode(s, Base64.DEFAULT);
|
||||
// TODO: check signature length
|
||||
|
||||
sig.initVerify(pubKey);
|
||||
sig.update(head.toString().getBytes());
|
||||
|
||||
boolean verified = sig.verify(signature);
|
||||
Log.i("DKIM valid=" + verified +
|
||||
" dns=" + dns +
|
||||
" from=" + formatAddresses(getFrom()));
|
||||
|
||||
if (verified &&
|
||||
!signers.contains(signer))
|
||||
signers.add(signer);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("DKIM", ex);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.i("DKIM error=" + ex);
|
||||
Log.e(ex);
|
||||
}
|
||||
|
||||
Log.i("DKIM signers=" + TextUtils.join(",", signers));
|
||||
} catch (Throwable ex) {
|
||||
Log.e("DKIM", ex);
|
||||
}
|
||||
|
||||
Log.i("DKIM passed=" + valid);
|
||||
return valid;
|
||||
return signers;
|
||||
}
|
||||
|
||||
Address[] getMailFrom(String[] headers) {
|
||||
|
|
|
@ -1639,6 +1639,30 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/tvLogarithmicBackoffHint"
|
||||
app:switchPadding="12dp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/swNativeDkim"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_native_dkim"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/swExactAlarms"
|
||||
app:switchPadding="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvNativeDkimHint"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:text="@string/title_advanced_sync_delay_hint"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="?attr/colorWarning"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/swNativeDkim" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/swInfra"
|
||||
android:layout_width="0dp"
|
||||
|
@ -1647,7 +1671,7 @@
|
|||
android:text="@string/title_advanced_infra"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/swExactAlarms"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvNativeDkimHint"
|
||||
app:switchPadding="12dp" />
|
||||
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
|
|
|
@ -131,6 +131,31 @@
|
|||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="ivPlain,ibReceipt,ivAutoSubmitted,ivBrowsed,ivRaw,ibSearchContact,ibNotifyContact,ibPinContact,ibAddContact" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSignedByTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:labelFor="@+id/tvSignedBy"
|
||||
android:text="@string/title_signed_by"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_action" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSignedBy"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="Submitter"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/barrier_addresses"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_action" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSubmitterTitle"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -141,7 +166,7 @@
|
|||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_action" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tvSignedBy" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSubmitter"
|
||||
|
@ -154,7 +179,7 @@
|
|||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/barrier_addresses"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_action" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tvSignedBy" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDeliveredToTitle"
|
||||
|
@ -487,6 +512,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="
|
||||
tvSignedByTitle,
|
||||
tvSubmitterTitle,tvDeliveredToTitle,
|
||||
tvFromExTitle,tvToTitle,tvReplyToTitle,
|
||||
tvCcTitle,tvBccTitle,
|
||||
|
|
|
@ -856,6 +856,7 @@
|
|||
<string name="title_advanced_keep_alive_poll" translatable="false">Poll on keep-alive</string>
|
||||
<string name="title_advanced_empty_pool" translatable="false">Empty connection pool</string>
|
||||
<string name="title_advanced_exact_alarms" translatable="false">Use exact timers</string>
|
||||
<string name="title_advanced_native_dkim" translatable="false">Native DKIM verification</string>
|
||||
<string name="title_advanced_infra" translatable="false">Show infrastructure</string>
|
||||
<string name="title_advanced_dup_msgid" translatable="false">Duplicates by message ID</string>
|
||||
<string name="title_advanced_global_keywords" translatable="false">Global keywords</string>
|
||||
|
@ -1485,6 +1486,7 @@
|
|||
|
||||
<string name="title_compose">Compose</string>
|
||||
<string name="title_submitter">Sent by:</string>
|
||||
<string name="title_signed_by">Signed by:</string>
|
||||
<string name="title_delivered_to">Delivered to:</string>
|
||||
<string name="title_from">From:</string>
|
||||
<string name="title_to">To:</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue