Experiment: identity/linked contact

pull/212/head
M66B 2 years ago
parent 0d883b50c8
commit fed1994b23

File diff suppressed because it is too large Load Diff

@ -68,7 +68,7 @@ import javax.mail.internet.InternetAddress;
// https://developer.android.com/topic/libraries/architecture/room.html // https://developer.android.com/topic/libraries/architecture/room.html
@Database( @Database(
version = 276, version = 277,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -2793,6 +2793,12 @@ public abstract class DB extends RoomDatabase {
logMigration(startVersion, endVersion); logMigration(startVersion, endVersion);
db.execSQL("ALTER TABLE `log` ADD COLUMN `thread` INTEGER"); db.execSQL("ALTER TABLE `log` ADD COLUMN `thread` INTEGER");
} }
}).addMigrations(new Migration(276, 277) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
logMigration(startVersion, endVersion);
db.execSQL("ALTER TABLE `identity` ADD COLUMN `uri` TEXT");
}
}).addMigrations(new Migration(998, 999) { }).addMigrations(new Migration(998, 999) {
@Override @Override
public void migrate(@NonNull SupportSQLiteDatabase db) { public void migrate(@NonNull SupportSQLiteDatabase db) {

@ -74,6 +74,8 @@ public class EntityAttachment {
static final Integer SMIME_SIGNED_DATA = 7; static final Integer SMIME_SIGNED_DATA = 7;
static final Integer SMIME_CONTENT = 8; static final Integer SMIME_CONTENT = 8;
static final String VCARD_PREFIX = BuildConfig.APPLICATION_ID + ".vcard.";
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
public Long id; public Long id;
@NonNull @NonNull

@ -108,6 +108,7 @@ public class EntityIdentity {
public String cc; public String cc;
public String bcc; public String bcc;
public String internal; public String internal;
public String uri; // linked contact
@NonNull @NonNull
public Boolean unicode = false; public Boolean unicode = false;
@NonNull @NonNull
@ -228,6 +229,7 @@ public class EntityIdentity {
json.put("cc", cc); json.put("cc", cc);
json.put("bcc", bcc); json.put("bcc", bcc);
json.put("internal", internal); json.put("internal", internal);
json.put("uri", uri);
json.put("unicode", unicode); json.put("unicode", unicode);
json.put("octetmime", octetmime); json.put("octetmime", octetmime);
@ -310,6 +312,8 @@ public class EntityIdentity {
identity.bcc = json.getString("bcc"); identity.bcc = json.getString("bcc");
if (json.has("internal") && !json.isNull("internal")) if (json.has("internal") && !json.isNull("internal"))
identity.internal = json.getString("internal"); identity.internal = json.getString("internal");
if (json.has("uri") && !json.isNull("uri"))
identity.uri = json.getString("uri");
if (json.has("unicode")) if (json.has("unicode"))
identity.unicode = json.getBoolean("unicode"); identity.unicode = json.getBoolean("unicode");
@ -366,6 +370,7 @@ public class EntityIdentity {
Objects.equals(i1.cc, other.cc) && Objects.equals(i1.cc, other.cc) &&
Objects.equals(i1.bcc, other.bcc) && Objects.equals(i1.bcc, other.bcc) &&
Objects.equals(i1.internal, other.internal) && Objects.equals(i1.internal, other.internal) &&
Objects.equals(i1.uri, other.uri) &&
Objects.equals(i1.unicode, other.unicode) && Objects.equals(i1.unicode, other.unicode) &&
Objects.equals(i1.octetmime, other.octetmime) && Objects.equals(i1.octetmime, other.octetmime) &&
// plain_only // plain_only

@ -31,6 +31,7 @@ import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -120,6 +121,8 @@ public class FragmentIdentity extends FragmentBase {
private EditText etCc; private EditText etCc;
private EditText etBcc; private EditText etBcc;
private EditText etInternal; private EditText etInternal;
private Button btnUri;
private TextView tvUri;
private CheckBox cbSignDefault; private CheckBox cbSignDefault;
private CheckBox cbEncryptDefault; private CheckBox cbEncryptDefault;
private CheckBox cbUnicode; private CheckBox cbUnicode;
@ -153,6 +156,7 @@ public class FragmentIdentity extends FragmentBase {
private static final int REQUEST_SAVE = 2; private static final int REQUEST_SAVE = 2;
private static final int REQUEST_DELETE = 3; private static final int REQUEST_DELETE = 3;
private static final int REQUEST_SIGNATURE = 4; private static final int REQUEST_SIGNATURE = 4;
private static final int REQUEST_URI = 5;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -220,6 +224,8 @@ public class FragmentIdentity extends FragmentBase {
etCc = view.findViewById(R.id.etCc); etCc = view.findViewById(R.id.etCc);
etBcc = view.findViewById(R.id.etBcc); etBcc = view.findViewById(R.id.etBcc);
etInternal = view.findViewById(R.id.etInternal); etInternal = view.findViewById(R.id.etInternal);
btnUri = view.findViewById(R.id.btnUri);
tvUri = view.findViewById(R.id.tvUri);
cbSignDefault = view.findViewById(R.id.cbSignDefault); cbSignDefault = view.findViewById(R.id.cbSignDefault);
cbEncryptDefault = view.findViewById(R.id.cbEncryptDefault); cbEncryptDefault = view.findViewById(R.id.cbEncryptDefault);
cbUnicode = view.findViewById(R.id.cbUnicode); cbUnicode = view.findViewById(R.id.cbUnicode);
@ -481,6 +487,15 @@ public class FragmentIdentity extends FragmentBase {
} }
}); });
btnUri.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent pick = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
pick.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(Helper.getChooser(getContext(), pick), REQUEST_URI);
}
});
cbEncryptDefault.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { cbEncryptDefault.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -527,6 +542,10 @@ public class FragmentIdentity extends FragmentBase {
cbInsecure.setVisibility(View.GONE); cbInsecure.setVisibility(View.GONE);
btnAdvanced.setVisibility(View.GONE); btnAdvanced.setVisibility(View.GONE);
if (!BuildConfig.DEBUG) {
Helper.hide(btnUri);
Helper.hide(tvUri);
}
etEhlo.setHint(EmailService.getDefaultEhlo()); etEhlo.setHint(EmailService.getDefaultEhlo());
@ -710,6 +729,7 @@ public class FragmentIdentity extends FragmentBase {
args.putString("cc", etCc.getText().toString().trim()); args.putString("cc", etCc.getText().toString().trim());
args.putString("bcc", etBcc.getText().toString().trim()); args.putString("bcc", etBcc.getText().toString().trim());
args.putString("internal", etInternal.getText().toString().replaceAll(" ", "")); args.putString("internal", etInternal.getText().toString().replaceAll(" ", ""));
args.putString("uri", tvUri.getText().toString());
args.putBoolean("sign_default", cbSignDefault.isChecked()); args.putBoolean("sign_default", cbSignDefault.isChecked());
args.putBoolean("encrypt_default", cbEncryptDefault.isChecked()); args.putBoolean("encrypt_default", cbEncryptDefault.isChecked());
args.putBoolean("unicode", cbUnicode.isChecked()); args.putBoolean("unicode", cbUnicode.isChecked());
@ -794,6 +814,7 @@ public class FragmentIdentity extends FragmentBase {
String cc = args.getString("cc"); String cc = args.getString("cc");
String bcc = args.getString("bcc"); String bcc = args.getString("bcc");
String internal = args.getString("internal"); String internal = args.getString("internal");
String uri = args.getString("uri");
boolean sign_default = args.getBoolean("sign_default"); boolean sign_default = args.getBoolean("sign_default");
boolean encrypt_default = args.getBoolean("encrypt_default"); boolean encrypt_default = args.getBoolean("encrypt_default");
boolean unicode = args.getBoolean("unicode"); boolean unicode = args.getBoolean("unicode");
@ -856,6 +877,9 @@ public class FragmentIdentity extends FragmentBase {
if (TextUtils.isEmpty(internal)) if (TextUtils.isEmpty(internal))
internal = null; internal = null;
if (TextUtils.isEmpty(uri))
uri = null;
if (TextUtils.isEmpty(display)) if (TextUtils.isEmpty(display))
display = null; display = null;
@ -949,6 +973,8 @@ public class FragmentIdentity extends FragmentBase {
return true; return true;
if (!Objects.equals(identity.internal, internal)) if (!Objects.equals(identity.internal, internal))
return true; return true;
if (!Objects.equals(identity.uri, uri))
return true;
if (!Objects.equals(identity.sign_default, sign_default)) if (!Objects.equals(identity.sign_default, sign_default))
return true; return true;
if (!Objects.equals(identity.encrypt_default, encrypt_default)) if (!Objects.equals(identity.encrypt_default, encrypt_default))
@ -1052,6 +1078,7 @@ public class FragmentIdentity extends FragmentBase {
identity.cc = cc; identity.cc = cc;
identity.bcc = bcc; identity.bcc = bcc;
identity.internal = internal; identity.internal = internal;
identity.uri = uri;
identity.sign_default = sign_default; identity.sign_default = sign_default;
identity.encrypt_default = encrypt_default; identity.encrypt_default = encrypt_default;
identity.unicode = unicode; identity.unicode = unicode;
@ -1238,6 +1265,7 @@ public class FragmentIdentity extends FragmentBase {
etCc.setText(identity == null ? null : identity.cc); etCc.setText(identity == null ? null : identity.cc);
etBcc.setText(identity == null ? null : identity.bcc); etBcc.setText(identity == null ? null : identity.bcc);
etInternal.setText(identity == null ? null : identity.internal); etInternal.setText(identity == null ? null : identity.internal);
tvUri.setText(identity == null ? null : identity.uri);
cbSignDefault.setChecked(identity != null && identity.sign_default); cbSignDefault.setChecked(identity != null && identity.sign_default);
cbEncryptDefault.setChecked(identity != null && identity.encrypt_default); cbEncryptDefault.setChecked(identity != null && identity.encrypt_default);
cbUnicode.setChecked(identity != null && identity.unicode); cbUnicode.setChecked(identity != null && identity.unicode);
@ -1444,6 +1472,9 @@ public class FragmentIdentity extends FragmentBase {
if (resultCode == RESULT_OK && data != null) if (resultCode == RESULT_OK && data != null)
onHtml(data.getExtras()); onHtml(data.getExtras());
break; break;
case REQUEST_URI:
onPickUri(resultCode == RESULT_OK ? data : null);
break;
} }
} catch (Throwable ex) { } catch (Throwable ex) {
Log.e(ex); Log.e(ex);
@ -1488,4 +1519,9 @@ public class FragmentIdentity extends FragmentBase {
private void onHtml(Bundle args) { private void onHtml(Bundle args) {
signature = args.getString("html"); signature = args.getString("html");
} }
private void onPickUri(Intent intent) {
Uri uri = (intent == null ? null : intent.getData());
tvUri.setText(uri == null ? null : uri.toString());
}
} }

@ -22,12 +22,15 @@ package eu.faircode.email;
import static android.system.OsConstants.ENOSPC; import static android.system.OsConstants.ENOSPC;
import android.Manifest; import android.Manifest;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.CalendarContract; import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.system.ErrnoException; import android.system.ErrnoException;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
@ -148,6 +151,11 @@ import biweekly.Biweekly;
import biweekly.ICalendar; import biweekly.ICalendar;
import biweekly.component.VEvent; import biweekly.component.VEvent;
import biweekly.property.Method; import biweekly.property.Method;
import ezvcard.VCard;
import ezvcard.VCardVersion;
import ezvcard.io.text.VCardWriter;
import ezvcard.parameter.AddressType;
import ezvcard.parameter.TelephoneType;
public class MessageHelper { public class MessageHelper {
private boolean ensuredEnvelope = false; private boolean ensuredEnvelope = false;
@ -1025,6 +1033,7 @@ public class MessageHelper {
reply.removeAttr("fairemail"); reply.removeAttr("fairemail");
DB db = DB.getInstance(context); DB db = DB.getInstance(context);
try { try {
db.beginTransaction(); db.beginTransaction();
@ -1079,6 +1088,161 @@ public class MessageHelper {
} finally { } finally {
db.endTransaction(); db.endTransaction();
} }
if (BuildConfig.DEBUG) {
VCard vcard = null;
if (identity.uri != null &&
Helper.hasPermission(context, Manifest.permission.READ_CONTACTS)) {
vcard = new VCard();
vcard.addEmail(identity.email);
if (!TextUtils.isEmpty(identity.name))
vcard.setFormattedName(identity.name);
ContentResolver resolver = context.getContentResolver();
try (Cursor cursor = resolver.query(Uri.parse(identity.uri),
new String[]{
ContactsContract.Contacts._ID
}, null, null, null)) {
if (cursor.moveToFirst()) {
String contactId = cursor.getString(0);
try (Cursor address = resolver.query(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
ContactsContract.CommonDataKinds.StructuredPostal.STREET,
ContactsContract.CommonDataKinds.StructuredPostal.POBOX,
ContactsContract.CommonDataKinds.StructuredPostal.CITY,
ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE,
ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY,
ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS,
},
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
new String[]{contactId}, null)) {
while (address.moveToNext()) {
int type = address.getInt(0);
if (type != ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME &&
type != ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK)
continue;
ezvcard.property.Address a = new ezvcard.property.Address();
if (!address.isNull(1))
a.setStreetAddress(address.getString(1));
if (!address.isNull(2))
a.setPoBox(address.getString(2));
if (!address.isNull(3))
a.setLocality(address.getString(3));
if (!address.isNull(4))
a.setPostalCode(address.getString(4));
if (!address.isNull(5))
a.setCountry(address.getString(5));
if (!address.isNull(6))
a.setLabel(address.getString(6));
switch (type) {
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
a.setParameter("TYPE", AddressType.HOME.getValue());
break;
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
a.setParameter("TYPE", AddressType.WORK.getValue());
break;
}
vcard.addAddress(a);
}
} catch (Throwable ex) {
Log.w(ex);
}
try (Cursor web = resolver.query(ContactsContract.Data.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Website.TYPE,
ContactsContract.CommonDataKinds.Website.URL
},
ContactsContract.Data.CONTACT_ID + " = " + contactId +
" AND " + ContactsContract.Contacts.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE + "'",
null, null)) {
while (web.moveToNext()) {
int type = web.getInt(0);
String url = web.getString(1);
vcard.addUrl(url);
}
} catch (Throwable ex) {
Log.w(ex);
}
try (Cursor phones = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.NUMBER
},
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId,
null, null)) {
while (phones.moveToNext()) {
int type = phones.getInt(0);
String number = phones.getString(1);
switch (type) {
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
vcard.addTelephoneNumber(number, TelephoneType.HOME);
break;
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
vcard.addTelephoneNumber(number, TelephoneType.WORK);
break;
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
vcard.addTelephoneNumber(number, TelephoneType.CELL);
break;
}
}
} catch (Throwable ex) {
Log.w(ex);
}
}
} catch (Throwable ex) {
Log.w(ex);
}
}
try {
db.beginTransaction();
for (EntityAttachment attachment : new ArrayList<>(attachments))
if (attachment.cid != null && attachment.cid.startsWith(EntityAttachment.VCARD_PREFIX)) {
db.attachment().deleteAttachment(attachment.id);
attachments.remove(attachment);
}
if (vcard != null) {
EntityAttachment attachment = new EntityAttachment();
attachment.message = message.id;
attachment.sequence = db.attachment().getAttachmentSequence(message.id) + 1;
attachment.name = "contact.vcf";
attachment.type = "text/vcard";
attachment.disposition = Part.ATTACHMENT;
attachment.cid = EntityAttachment.VCARD_PREFIX + Math.abs(identity.uri.hashCode());
attachment.size = null;
attachment.progress = 0;
attachment.id = db.attachment().insertAttachment(attachment);
File file = attachment.getFile(context);
try (VCardWriter writer = new VCardWriter(file, VCardVersion.V3_0)) {
writer.write(vcard);
}
attachment.size = file.length();
attachment.progress = null;
attachment.available = true;
db.attachment().setDownloaded(attachment.id, attachment.size);
attachments.add(attachment);
}
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.w(ex);
} finally {
db.endTransaction();
}
}
} }
} }

@ -774,6 +774,27 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInternal" /> app:layout_constraintTop_toBottomOf="@id/etInternal" />
<Button
android:id="@+id/btnUri"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:drawableEnd="@drawable/twotone_person_24"
android:drawablePadding="6dp"
android:text="@string/title_identity_uri"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInternalHint" />
<TextView
android:id="@+id/tvUri"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Contact URI"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnUri" />
<TextView <TextView
android:id="@+id/tvE2Encryption" android:id="@+id/tvE2Encryption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -782,7 +803,7 @@
android:text="@string/title_advanced_e2e_encryption" android:text="@string/title_advanced_e2e_encryption"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInternalHint" /> app:layout_constraintTop_toBottomOf="@id/tvUri" />
<CheckBox <CheckBox
android:id="@+id/cbSignDefault" android:id="@+id/cbSignDefault"
@ -986,6 +1007,7 @@
cbSenderExtra,cbSenderExtraName,cbReplyExtraName,tvSenderExtra,etSenderExtra,ibSenderExtra,tvSenderExtraHint, cbSenderExtra,cbSenderExtraName,cbReplyExtraName,tvSenderExtra,etSenderExtra,ibSenderExtra,tvSenderExtraHint,
tvReplyTo,etReplyTo,tvCc,etCc,tvCcHint,tvBcc,etBcc,tvBccHint, tvReplyTo,etReplyTo,tvCc,etCc,tvCcHint,tvBcc,etBcc,tvBccHint,
tvInternal,etInternal,tvInternalHint, tvInternal,etInternal,tvInternalHint,
btnUri,tvUri,
tvE2Encryption,cbSignDefault,cbEncryptDefault, tvE2Encryption,cbSignDefault,cbEncryptDefault,
cbUnicode,tvUnicodeHint,cbOctetMime,tvMaxSize,etMaxSize" /> cbUnicode,tvUnicodeHint,cbOctetMime,tvMaxSize,etMaxSize" />

@ -1067,6 +1067,7 @@
<string name="title_advanced_sender_regex">Regex to match username of incoming email addresses</string> <string name="title_advanced_sender_regex">Regex to match username of incoming email addresses</string>
<string name="title_identity_reply_to">Reply to address</string> <string name="title_identity_reply_to">Reply to address</string>
<string name="title_identity_internal">Internal domain names (comma separated)</string> <string name="title_identity_internal">Internal domain names (comma separated)</string>
<string name="title_identity_uri" translatable="false">Link contact</string>
<string name="title_identity_unicode">Allow UTF-8 in message headers</string> <string name="title_identity_unicode">Allow UTF-8 in message headers</string>
<string name="title_identity_unicode_remark">Most servers do not support this</string> <string name="title_identity_unicode_remark">Most servers do not support this</string>
<string name="title_identity_octetmime">Allow 8BITMIME</string> <string name="title_identity_octetmime">Allow 8BITMIME</string>

Loading…
Cancel
Save