Show message signers

pull/212/head
M66B 2 years ago
parent bf91c80b7e
commit 1bd7b4cfdb

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;
// 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;
boolean valid = false;
for (String header : headers) {
Map<String, String> kv = getKeyValues(MimeUtility.unfold(header));
@NonNull
List<String> verifyDKIM(Context context) {
List<String> signers = new ArrayList<>();
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 {
// 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 signers;
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);
continue;
}
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;
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", "|"));
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", "|"));
if ("simple".equals(c[0])) {
if ("DKIM-Signature".equals(n))
head.append(n).append(": ").append(v);
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 {
// 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());
}
while (body.endsWith("\r\n\r\n"))
body = body.substring(0, body.length() - 2);
}
} 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))
} else if ("relaxed".equals(c[c.length > 1 ? 1 : 0])) {
if (TextUtils.isEmpty(body))
body = "";
}
} else
throw new IllegalArgumentException(c[1]);
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", "|"));
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"));
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);
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=
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);
String s = kv.get("b").replaceAll("\\s+", "");
Log.i("DKIM signature=" + s);
byte[] signature = Base64.decode(s, Base64.DEFAULT);
// TODO: check signature length
byte[] signature = Base64.decode(s, Base64.DEFAULT);
// TODO: check signature length
sig.initVerify(pubKey);
sig.update(head.toString().getBytes());
sig.initVerify(pubKey);
sig.update(head.toString().getBytes());
boolean verified = sig.verify(signature);
Log.i("DKIM valid=" + verified +
" dns=" + dns +
" from=" + formatAddresses(getFrom()));
boolean verified = sig.verify(signature);
Log.i("DKIM valid=" + verified +
" dns=" + dns +
" from=" + formatAddresses(getFrom()));
if (verified)
valid = true;
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…
Cancel
Save