First public release

This commit is contained in:
M66B
2018-08-02 13:33:06 +00:00
commit 00e01c2ecd
1018 changed files with 10081 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
abstract class ActivityBase extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String theme = prefs.getString("theme", "light");
setTheme("dark".equals(theme) ? R.style.AppThemeDark : R.style.AppThemeLight);
super.onCreate(savedInstanceState);
}
}

View File

@@ -0,0 +1,57 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
public class ActivityCompose extends ActivityBase implements FragmentManager.OnBackStackChangedListener {
static final int LOADER_COMPOSE_GET = 1;
static final int LOADER_COMPOSE_PUT = 2;
static final int LOADER_COMPOSE_DELETE = 3;
static final int REQUEST_CONTACT = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compose);
getSupportFragmentManager().addOnBackStackChangedListener(this);
if (getSupportFragmentManager().getFragments().size() == 0) {
FragmentCompose fragment = new FragmentCompose();
Bundle args = getIntent().getExtras();
if (args == null)
args = new Bundle();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("compose");
fragmentTransaction.commit();
}
}
@Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
finish();
}
}

View File

@@ -0,0 +1,46 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
public class ActivitySetup extends ActivityBase implements FragmentManager.OnBackStackChangedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setup);
getSupportFragmentManager().addOnBackStackChangedListener(this);
if (getSupportFragmentManager().getFragments().size() == 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentSetup()).addToBackStack("setup");
fragmentTransaction.commit();
}
}
@Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
finish();
}
}

View File

@@ -0,0 +1,388 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;
import java.util.List;
public class ActivityView extends ActivityBase implements FragmentManager.OnBackStackChangedListener, SharedPreferences.OnSharedPreferenceChangeListener {
private DrawerLayout drawerLayout;
private ListView drawerList;
private ActionBarDrawerToggle drawerToggle;
static final int LOADER_ACCOUNT_PUT = 1;
static final int LOADER_IDENTITY_PUT = 2;
static final int LOADER_FOLDER_PUT = 3;
static final int REQUEST_VIEW = 1;
static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES";
static final String ACTION_VIEW_MESSAGE = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGE";
static final String ACTION_EDIT_FOLDER = BuildConfig.APPLICATION_ID + ".EDIT_FOLDER";
static final String ACTION_EDIT_ACCOUNT = BuildConfig.APPLICATION_ID + ".EDIT_ACCOUNT";
static final String ACTION_EDIT_IDENTITY = BuildConfig.APPLICATION_ID + ".EDIT_IDENTITY";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
drawerLayout = findViewById(R.id.drawer_layout);
drawerLayout.setScrimColor(Helper.resolveColor(this, R.attr.colorDrawerScrim));
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.app_name, R.string.app_name) {
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getSupportActionBar().setTitle(getString(R.string.app_name));
}
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getSupportActionBar().setTitle(getString(R.string.app_name));
}
};
drawerLayout.addDrawerListener(drawerToggle);
drawerList = findViewById(R.id.drawer_list);
drawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
DrawerItem item = (DrawerItem) parent.getAdapter().getItem(position);
switch (item.getId()) {
case R.string.menu_unified:
onMenuUnified();
break;
case R.string.menu_folders:
onMenuFolders();
break;
case R.string.menu_accounts:
onMenuAccounts();
break;
case R.string.menu_identities:
onMenuIdentities();
break;
case R.string.menu_theme:
onMenuTheme();
break;
case R.string.menu_setup:
onMenuSetup();
break;
}
if (!item.isCheckable())
drawerLayout.closeDrawer(drawerList);
}
});
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);
getSupportFragmentManager().addOnBackStackChangedListener(this);
updateDrawer();
if (getSupportFragmentManager().getFragments().size() == 0)
init();
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
drawerToggle.syncState();
}
@Override
protected void onResume() {
super.onResume();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
IntentFilter iff = new IntentFilter();
iff.addAction(ACTION_VIEW_MESSAGES);
iff.addAction(ACTION_VIEW_MESSAGE);
iff.addAction(ACTION_EDIT_FOLDER);
iff.addAction(ACTION_EDIT_ACCOUNT);
iff.addAction(ACTION_EDIT_IDENTITY);
lbm.registerReceiver(receiver, iff);
}
@Override
protected void onPause() {
super.onPause();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
lbm.unregisterReceiver(receiver);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
public void onBackPressed() {
if (drawerLayout.isDrawerOpen(drawerList))
drawerLayout.closeDrawer(drawerList);
else
super.onBackPressed();
}
@Override
protected void onDestroy() {
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if ("eula".equals(key))
init();
}
@Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (drawerToggle.onOptionsItemSelected(item))
return true;
return false;
}
private void init() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (prefs.getBoolean("eula", false)) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
FragmentMessages fragment = new FragmentMessages();
Bundle args = new Bundle();
args.putLong("folder", -1);
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("unified");
fragmentTransaction.commit();
Fragment eula = getSupportFragmentManager().findFragmentByTag("eula");
if (eula != null)
getSupportFragmentManager().beginTransaction().remove(eula).commit();
DB.getInstance(this).account().liveAccounts(true).observe(this, new Observer<List<EntityAccount>>() {
@Override
public void onChanged(@Nullable List<EntityAccount> accounts) {
if (accounts.size() == 0)
startActivity(new Intent(ActivityView.this, ActivitySetup.class));
else
ServiceSynchronize.start(ActivityView.this);
}
});
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentEula(), "eula");
fragmentTransaction.commit();
}
}
public void updateDrawer() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
ArrayAdapterDrawer drawerArray = new ArrayAdapterDrawer(this, R.layout.item_drawer);
drawerArray.add(new DrawerItem(ActivityView.this, R.string.menu_unified));
drawerArray.add(new DrawerItem(ActivityView.this, R.string.menu_folders));
drawerArray.add(new DrawerItem(ActivityView.this, R.string.menu_accounts));
drawerArray.add(new DrawerItem(ActivityView.this, R.string.menu_identities));
drawerArray.add(new DrawerItem(ActivityView.this, R.string.menu_theme, "dark".equals(prefs.getString("theme", "light"))));
drawerArray.add(new DrawerItem(ActivityView.this, R.string.menu_setup));
drawerList.setAdapter(drawerArray);
}
private void onMenuUnified() {
FragmentMessages fragment = new FragmentMessages();
Bundle args = new Bundle();
args.putLong("folder", -1);
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("unified");
fragmentTransaction.commit();
}
private void onMenuFolders() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentFolders()).addToBackStack("folders");
fragmentTransaction.commit();
}
private void onMenuAccounts() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentAccounts()).addToBackStack("accounts");
fragmentTransaction.commit();
}
private void onMenuIdentities() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentIdentities()).addToBackStack("identities");
fragmentTransaction.commit();
}
private void onMenuTheme() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String theme = prefs.getString("theme", "light");
theme = ("dark".equals(theme) ? "light" : "dark");
prefs.edit().putString("theme", theme).apply();
recreate();
}
private void onMenuSetup() {
startActivity(new Intent(ActivityView.this, ActivitySetup.class));
}
private class DrawerItem {
private int id;
private String title;
private boolean checkable;
private boolean checked;
DrawerItem(Context context, int title) {
this.id = title;
this.title = context.getString(title);
this.checkable = false;
this.checked = false;
}
DrawerItem(Context context, int title, boolean checked) {
this.id = title;
this.title = context.getString(title);
this.checkable = true;
this.checked = checked;
}
public int getId() {
return this.id;
}
public String getTitle() {
return this.title;
}
public boolean isCheckable() {
return this.checkable;
}
public boolean isChecked() {
return this.checked;
}
}
private static class ArrayAdapterDrawer extends ArrayAdapter<DrawerItem> {
private int resource;
ArrayAdapterDrawer(@NonNull Context context, int resource) {
super(context, resource);
this.resource = resource;
}
@NonNull
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View row;
if (null == convertView)
row = LayoutInflater.from(getContext()).inflate(this.resource, null);
else
row = convertView;
DrawerItem item = getItem(position);
TextView tv = row.findViewById(R.id.tvItem);
CheckBox cb = row.findViewById(R.id.cbItem);
tv.setText(item.getTitle());
cb.setVisibility(item.isCheckable() ? View.VISIBLE : View.GONE);
cb.setChecked(item.isChecked());
return row;
}
}
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_VIEW_MESSAGES.equals(intent.getAction())) {
FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("messages");
fragmentTransaction.commit();
} else if (ACTION_VIEW_MESSAGE.equals(intent.getAction())) {
FragmentMessage fragment = new FragmentMessage();
fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("message");
fragmentTransaction.commit();
} else if (ACTION_EDIT_FOLDER.equals(intent.getAction())) {
FragmentFolder fragment = new FragmentFolder();
fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("folder");
fragmentTransaction.commit();
} else if (ACTION_EDIT_ACCOUNT.equals(intent.getAction())) {
FragmentAccount fragment = new FragmentAccount();
fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
fragmentTransaction.commit();
} else if (ACTION_EDIT_IDENTITY.equals(intent.getAction())) {
FragmentIdentity fragment = new FragmentIdentity();
fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
fragmentTransaction.commit();
}
}
};
}

View File

@@ -0,0 +1,201 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHolder> {
private Context context;
private List<EntityAccount> all = new ArrayList<>();
private List<EntityAccount> filtered = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
View itemView;
ImageView ivPrimary;
TextView tvName;
ImageView ivSync;
TextView tvHost;
TextView tvUser;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
ivPrimary = itemView.findViewById(R.id.ivPrimary);
tvName = itemView.findViewById(R.id.tvName);
ivSync = itemView.findViewById(R.id.ivSync);
tvHost = itemView.findViewById(R.id.tvHost);
tvUser = itemView.findViewById(R.id.tvUser);
}
private void wire() {
itemView.setOnClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
}
@Override
public void onClick(View view) {
EntityAccount account = filtered.get(getLayoutPosition());
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_EDIT_ACCOUNT)
.putExtra("id", account.id));
}
}
AdapterAccount(Context context) {
this.context = context;
setHasStableIds(true);
}
public void set(List<EntityAccount> accounts) {
Log.i(Helper.TAG, "Set accounts=" + accounts.size());
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
Collections.sort(accounts, new Comparator<EntityAccount>() {
@Override
public int compare(EntityAccount a1, EntityAccount a2) {
return collator.compare(a1.host, a2.host);
}
});
all.clear();
all.addAll(accounts);
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
filtered.clear();
filtered.addAll(all);
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.i(Helper.TAG, "Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.i(Helper.TAG, "Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.i(Helper.TAG, "Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.i(Helper.TAG, "Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(AdapterAccount.this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<EntityAccount> prev;
private List<EntityAccount> next;
MessageDiffCallback(List<EntityAccount> prev, List<EntityAccount> next) {
this.prev = prev;
this.next = next;
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
EntityAccount f1 = prev.get(oldItemPosition);
EntityAccount f2 = next.get(newItemPosition);
return f1.id.equals(f2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntityAccount f1 = prev.get(oldItemPosition);
EntityAccount f2 = next.get(newItemPosition);
return f1.equals(f2);
}
}
@Override
public long getItemId(int position) {
return filtered.get(position).id;
}
@Override
public int getItemCount() {
return filtered.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_account, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
EntityAccount account = filtered.get(position);
holder.ivPrimary.setVisibility(account.primary ? View.VISIBLE : View.GONE);
holder.tvName.setText(account.name);
holder.ivSync.setVisibility(account.synchronize ? View.VISIBLE : View.INVISIBLE);
holder.tvHost.setText(String.format("%s:%d", account.host, account.port));
holder.tvUser.setText(account.user);
holder.wire();
}
}

View File

@@ -0,0 +1,238 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder> {
private Context context;
private List<TupleFolderEx> all = new ArrayList<>();
private List<TupleFolderEx> filtered = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener, View.OnLongClickListener {
View itemView;
TextView tvName;
TextView tvAfter;
ImageView ivSync;
TextView tvCount;
TextView tvType;
TextView tvAccount;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
tvName = itemView.findViewById(R.id.tvName);
tvAfter = itemView.findViewById(R.id.tvAfter);
ivSync = itemView.findViewById(R.id.ivSync);
tvCount = itemView.findViewById(R.id.tvCount);
tvType = itemView.findViewById(R.id.tvType);
tvAccount = itemView.findViewById(R.id.tvAccount);
}
private void wire() {
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
itemView.setOnLongClickListener(null);
}
@Override
public void onClick(View view) {
TupleFolderEx folder = filtered.get(getLayoutPosition());
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGES)
.putExtra("folder", folder.id));
}
@Override
public boolean onLongClick(View view) {
TupleFolderEx folder = filtered.get(getLayoutPosition());
if (!EntityFolder.TYPE_OUTBOX.equals(folder.type)) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_EDIT_FOLDER)
.putExtra("id", folder.id));
return true;
}
return false;
}
}
AdapterFolder(Context context) {
this.context = context;
setHasStableIds(true);
}
public void set(List<TupleFolderEx> folders) {
Log.i(Helper.TAG, "Set folders=" + folders.size());
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
Collections.sort(folders, new Comparator<TupleFolderEx>() {
@Override
public int compare(TupleFolderEx f1, TupleFolderEx f2) {
if (f1.accountName == null)
if (f2.accountName == null)
return 0;
else
return -1;
else if (f2.accountName == null)
return 1;
else
return collator.compare(f1.accountName, f2.accountName);
}
});
all.clear();
all.addAll(folders);
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
filtered.clear();
filtered.addAll(all);
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.i(Helper.TAG, "Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.i(Helper.TAG, "Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.i(Helper.TAG, "Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.i(Helper.TAG, "Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(AdapterFolder.this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<TupleFolderEx> prev;
private List<TupleFolderEx> next;
MessageDiffCallback(List<TupleFolderEx> prev, List<TupleFolderEx> next) {
this.prev = prev;
this.next = next;
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
TupleFolderEx f1 = prev.get(oldItemPosition);
TupleFolderEx f2 = next.get(newItemPosition);
return f1.id.equals(f2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
TupleFolderEx f1 = prev.get(oldItemPosition);
TupleFolderEx f2 = next.get(newItemPosition);
return f1.equals(f2);
}
}
@Override
public long getItemId(int position) {
return filtered.get(position).id;
}
@Override
public int getItemCount() {
return filtered.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_folder, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
TupleFolderEx folder = filtered.get(position);
String name = Helper.localizeFolderName(context, folder.name);
if (folder.unseen > 0)
holder.tvName.setText(context.getString(R.string.title_folder_unseen, name, folder.unseen));
else
holder.tvName.setText(name);
holder.tvName.setTypeface(null, folder.unseen > 0 ? Typeface.BOLD : Typeface.NORMAL);
holder.tvAfter.setText(Integer.toString(folder.after));
holder.tvAfter.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
holder.ivSync.setVisibility(folder.synchronize ? View.VISIBLE : View.INVISIBLE);
holder.tvCount.setText(Integer.toString(folder.messages));
holder.tvType.setText(folder.type);
holder.tvAccount.setText(folder.accountName);
holder.wire();
}
}

View File

@@ -0,0 +1,201 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class AdapterIdentity extends RecyclerView.Adapter<AdapterIdentity.ViewHolder> {
private Context context;
private List<EntityIdentity> all = new ArrayList<>();
private List<EntityIdentity> filtered = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
View itemView;
ImageView ivPrimary;
TextView tvName;
ImageView ivSync;
TextView tvHost;
TextView tvUser;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
ivPrimary = itemView.findViewById(R.id.ivPrimary);
tvName = itemView.findViewById(R.id.tvName);
ivSync = itemView.findViewById(R.id.ivSync);
tvHost = itemView.findViewById(R.id.tvHost);
tvUser = itemView.findViewById(R.id.tvUser);
}
private void wire() {
itemView.setOnClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
}
@Override
public void onClick(View view) {
EntityIdentity identity = filtered.get(getLayoutPosition());
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_EDIT_IDENTITY)
.putExtra("id", identity.id));
}
}
AdapterIdentity(Context context) {
this.context = context;
setHasStableIds(true);
}
public void set(List<EntityIdentity> identities) {
Log.i(Helper.TAG, "Set identities=" + identities.size());
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
Collections.sort(identities, new Comparator<EntityIdentity>() {
@Override
public int compare(EntityIdentity i1, EntityIdentity i2) {
return collator.compare(i1.host, i2.host);
}
});
all.clear();
all.addAll(identities);
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
filtered.clear();
filtered.addAll(all);
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.i(Helper.TAG, "Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.i(Helper.TAG, "Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.i(Helper.TAG, "Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.i(Helper.TAG, "Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(AdapterIdentity.this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<EntityIdentity> prev;
private List<EntityIdentity> next;
MessageDiffCallback(List<EntityIdentity> prev, List<EntityIdentity> next) {
this.prev = prev;
this.next = next;
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
EntityIdentity i1 = prev.get(oldItemPosition);
EntityIdentity i2 = next.get(newItemPosition);
return i1.id.equals(i2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntityIdentity i1 = prev.get(oldItemPosition);
EntityIdentity i2 = next.get(newItemPosition);
return i1.equals(i2);
}
}
@Override
public long getItemId(int position) {
return filtered.get(position).id;
}
@Override
public int getItemCount() {
return filtered.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_identity, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
EntityIdentity identity = filtered.get(position);
holder.ivPrimary.setVisibility(identity.primary ? View.VISIBLE : View.GONE);
holder.tvName.setText(identity.name);
holder.ivSync.setVisibility(identity.synchronize ? View.VISIBLE : View.INVISIBLE);
holder.tvHost.setText(String.format("%s:%d", identity.host, identity.port));
holder.tvUser.setText(identity.user);
holder.wire();
}
}

View File

@@ -0,0 +1,242 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHolder> {
private Context context;
private List<TupleMessageEx> all = new ArrayList<>();
private List<TupleMessageEx> filtered = new ArrayList<>();
private ExecutorService executor = Executors.newCachedThreadPool();
enum ViewType {FOLDER, THREAD}
private ViewType viewType;
public class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
View itemView;
TextView tvAddress;
TextView tvTime;
TextView tvSubject;
TextView tvCount;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
tvAddress = itemView.findViewById(R.id.tvAddress);
tvTime = itemView.findViewById(R.id.tvTime);
tvSubject = itemView.findViewById(R.id.tvSubject);
tvCount = itemView.findViewById(R.id.tvCount);
}
private void wire() {
itemView.setOnClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
}
@Override
public void onClick(View view) {
final TupleMessageEx message = filtered.get(getLayoutPosition());
executor.submit(new Runnable() {
@Override
public void run() {
if (EntityFolder.TYPE_DRAFTS.equals(message.folderType))
context.startActivity(
new Intent(context, ActivityCompose.class)
.putExtra("id", message.id));
else {
if (!message.seen && !message.ui_seen) {
message.ui_seen = !message.ui_seen;
DB.getInstance(context).message().updateMessage(message);
EntityOperation.queue(context, message, EntityOperation.SEEN, message.ui_seen);
}
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGE)
.putExtra("folder", message.folder)
.putExtra("id", message.id));
}
}
});
}
}
AdapterMessage(Context context, ViewType viewType) {
this.context = context;
this.viewType = viewType;
setHasStableIds(true);
}
public void set(List<TupleMessageEx> messages) {
Log.i(Helper.TAG, "Set messages=" + messages.size());
Collections.sort(messages, new Comparator<TupleMessageEx>() {
@Override
public int compare(TupleMessageEx m1, TupleMessageEx m2) {
if (EntityFolder.isOutgoing(m1.folderType))
return -Long.compare(m1.received, m2.received);
else
return -Long.compare(m1.sent, m2.sent);
}
});
all.clear();
all.addAll(messages);
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
filtered.clear();
filtered.addAll(all);
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.i(Helper.TAG, "Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.i(Helper.TAG, "Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.i(Helper.TAG, "Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.i(Helper.TAG, "Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(AdapterMessage.this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<TupleMessageEx> prev;
private List<TupleMessageEx> next;
MessageDiffCallback(List<TupleMessageEx> prev, List<TupleMessageEx> next) {
this.prev = prev;
this.next = next;
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
TupleMessageEx m1 = prev.get(oldItemPosition);
TupleMessageEx m2 = next.get(newItemPosition);
return m1.id.equals(m2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
TupleMessageEx m1 = prev.get(oldItemPosition);
TupleMessageEx m2 = next.get(newItemPosition);
return m1.equals(m2);
}
}
@Override
public long getItemId(int position) {
return filtered.get(position).id;
}
@Override
public int getItemCount() {
return filtered.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_message, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
TupleMessageEx message = filtered.get(position);
if (EntityFolder.isOutgoing(message.folderType)) {
holder.tvAddress.setText(message.to == null ? null : MessageHelper.getFormattedAddresses(message.to));
holder.tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
} else {
holder.tvAddress.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from));
holder.tvTime.setText(message.sent == null ? null : DateUtils.getRelativeTimeSpanString(context, message.sent));
}
holder.tvSubject.setText(message.subject);
if (viewType == ViewType.FOLDER) {
holder.tvCount.setText(Integer.toString(message.count));
holder.tvCount.setVisibility(message.count > 1 ? View.VISIBLE : View.GONE);
} else
holder.tvCount.setText(Helper.localizeFolderName(context, message.folderName));
boolean unseen = (message.thread == null ? !message.seen : message.unseen > 0);
int visibility = (unseen ? Typeface.BOLD : Typeface.NORMAL);
holder.tvAddress.setTypeface(null, visibility);
holder.tvTime.setTypeface(null, visibility);
holder.tvSubject.setTypeface(null, visibility);
holder.tvCount.setTypeface(null, visibility);
holder.wire();
}
}

View File

@@ -0,0 +1,25 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.app.Application;
public class ApplicationEx extends Application {
}

View File

@@ -0,0 +1,94 @@
package eu.faircode.email;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.TypeConverter;
import android.arch.persistence.room.TypeConverters;
import android.arch.persistence.room.migration.Migration;
import android.content.Context;
import android.util.Log;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
entities = {
EntityIdentity.class,
EntityAccount.class,
EntityFolder.class,
EntityMessage.class,
EntityOperation.class
},
version = 1,
exportSchema = true
)
@TypeConverters({DB.Converters.class})
public abstract class DB extends RoomDatabase {
public abstract DaoIdentity identity();
public abstract DaoAccount account();
public abstract DaoFolder folder();
public abstract DaoMessage message();
public abstract DaoOperation operation();
private static DB sInstance;
private static final String DB_NAME = "email.db";
public static synchronized DB getInstance(Context context) {
if (sInstance == null)
sInstance = migrate(Room.databaseBuilder(context.getApplicationContext(), DB.class, DB_NAME));
return sInstance;
}
private static DB migrate(RoomDatabase.Builder<DB> builder) {
return builder
//.addMigrations(MIGRATION_1_2)
.build();
}
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE message ADD COLUMN error TEXT");
}
};
public static class Converters {
@TypeConverter
public static byte[] fromString(String value) {
return null;
}
@TypeConverter
public static String fromBytes(byte[] value) {
return null;
}
}
}

View File

@@ -0,0 +1,65 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface DaoAccount {
@Query("SELECT * FROM account WHERE synchronize = :synchronize")
List<EntityAccount> getAccounts(boolean synchronize);
@Query("SELECT * FROM account")
LiveData<List<EntityAccount>> liveAccounts();
@Query("SELECT * FROM account WHERE synchronize = :synchronize")
LiveData<List<EntityAccount>> liveAccounts(boolean synchronize);
@Query("SELECT * FROM account WHERE id = :id")
EntityAccount getAccount(long id);
@Query("SELECT * FROM account WHERE id = :id")
LiveData<EntityAccount> liveAccount(long id);
@Query("SELECT * FROM account ORDER BY id LIMIT 1")
LiveData<EntityAccount> liveFirstAccount();
@Query("SELECT" +
" (SELECT COUNT(*) FROM account WHERE synchronize) AS accounts," +
" (SELECT COUNT(*) FROM operation JOIN message ON message.id = operation.message JOIN account ON account.id = message.account WHERE synchronize) AS operations")
LiveData<TupleAccountStats> liveStats();
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertAccount(EntityAccount account);
@Update
void updateAccount(EntityAccount account);
@Query("UPDATE account SET `primary` = 0")
void resetPrimary();
}

View File

@@ -0,0 +1,90 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface DaoFolder {
@Query("SELECT * FROM folder WHERE account = :account AND synchronize = :synchronize")
List<EntityFolder> getFolders(long account, boolean synchronize);
@Query("SELECT * FROM folder WHERE account = :account AND type = '" + EntityFolder.TYPE_USER + "'")
List<EntityFolder> getUserFolders(long account);
@Query("SELECT folder.*, account.name AS accountName" +
", COUNT(message.id) AS messages" +
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
" FROM folder" +
" LEFT JOIN account ON account.id = folder.account" +
" LEFT JOIN message ON message.folder = folder.id AND NOT message.ui_hide" +
" GROUP BY folder.id")
LiveData<List<TupleFolderEx>> liveFolders();
@Query("SELECT folder.* FROM folder WHERE folder.id = :id")
LiveData<EntityFolder> liveFolder(long id);
@Query("SELECT folder.*, account.name AS accountName" +
", COUNT(message.id) AS messages" +
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
" FROM folder" +
" LEFT JOIN account ON account.id = folder.account" +
" LEFT JOIN message ON message.folder = folder.id AND NOT message.ui_hide" +
" WHERE folder.id = :id")
LiveData<TupleFolderEx> liveFolderEx(long id);
@Query("SELECT * FROM folder WHERE id = :id")
EntityFolder getFolder(Long id);
@Query("SELECT * FROM folder WHERE account = :account AND name = :name")
EntityFolder getFolder(Long account, String name);
@Query("SELECT folder.* FROM folder" +
" JOIN account ON account.id = folder.account" +
" WHERE account.`primary` AND type = '" + EntityFolder.TYPE_DRAFTS + "' ")
EntityFolder getPrimaryDraftFolder();
@Query("SELECT folder.* FROM folder" +
" WHERE account = :account AND type = '" + EntityFolder.TYPE_ARCHIVE + "' ")
EntityFolder getArchiveFolder(long account);
@Query("SELECT folder.* FROM folder" +
" WHERE account = :account AND type = '" + EntityFolder.TYPE_JUNK + "' ")
EntityFolder getSpamFolder(long account);
@Query("SELECT * FROM folder WHERE type = '" + EntityFolder.TYPE_OUTBOX + "'")
EntityFolder getOutbox();
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertFolder(EntityFolder folder);
@Update
void updateFolder(EntityFolder folder);
@Query("DELETE FROM folder WHERE account= :account AND name = :name")
int deleteFolder(Long account, String name);
}

View File

@@ -0,0 +1,56 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface DaoIdentity {
@Query("SELECT * FROM identity")
LiveData<List<EntityIdentity>> liveIdentities();
@Query("SELECT * FROM identity WHERE synchronize = :synchronize")
LiveData<List<EntityIdentity>> liveIdentities(boolean synchronize);
@Query("SELECT * FROM identity WHERE id = :id")
EntityIdentity getIdentity(long id);
@Query("SELECT * FROM identity WHERE id = :id")
LiveData<EntityIdentity> liveIdentity(long id);
@Query("SELECT * FROM identity ORDER BY id LIMIT 1")
LiveData<EntityIdentity> liveFirstIdentity();
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertIdentity(EntityIdentity identity);
@Update
void updateIdentity(EntityIdentity identity);
@Query("UPDATE identity SET `primary` = 0")
void resetPrimary();
}

View File

@@ -0,0 +1,96 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface DaoMessage {
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
" FROM folder" +
" JOIN message ON folder = folder.id" +
" WHERE folder.type = '" + EntityFolder.TYPE_INBOX + "'" +
" AND NOT ui_hide" +
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder GROUP BY m.thread)")
LiveData<List<TupleMessageEx>> liveUnifiedInbox();
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
" FROM folder" +
" JOIN message ON folder = folder.id" +
" WHERE folder.id = :folder" +
" AND NOT ui_hide" +
" AND received IN (SELECT MAX(m.received) FROM message m WHERE m.folder = message.folder GROUP BY m.thread)")
LiveData<List<TupleMessageEx>> liveMessages(long folder);
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
" FROM message" +
" JOIN folder ON folder.id = message.folder" +
" JOIN message m1 ON m1.id = :msgid AND m1.account = message.account AND m1.thread = message.thread" +
" WHERE NOT message.ui_hide")
LiveData<List<TupleMessageEx>> liveThread(long msgid);
@Query("SELECT * FROM message WHERE id = :id")
EntityMessage getMessage(long id);
@Query("SELECT * FROM message WHERE folder = :folder AND uid = :uid")
EntityMessage getMessage(long folder, long uid);
@Query("SELECT message.*, folder.name as folderName, folder.type as folderType" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread) AS count" +
", (SELECT COUNT(m.id) FROM message m WHERE m.account = message.account AND m.thread = message.thread AND NOT m.ui_seen) AS unseen" +
" FROM message" +
" JOIN folder ON folder.id = message.folder" +
" WHERE message.id = :id")
LiveData<TupleMessageEx> liveMessage(long id);
@Query("SELECT uid FROM message WHERE folder = :folder AND received >= :received AND NOT uid IS NULL")
List<Long> getUids(long folder, long received);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertMessage(EntityMessage message);
@Update
void updateMessage(EntityMessage message);
@Query("DELETE FROM message WHERE id = :id")
void deleteMessage(long id);
@Query("DELETE FROM message WHERE folder = :folder AND uid = :uid")
void deleteMessage(long folder, long uid);
@Query("DELETE FROM message WHERE folder = :folder")
int deleteMessages(long folder);
@Query("DELETE FROM message WHERE folder = :folder AND received < :received AND NOT uid IS NULL")
int deleteMessagesBefore(long folder, long received);
}

View File

@@ -0,0 +1,45 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import java.util.List;
@Dao
public interface DaoOperation {
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertOperation(EntityOperation operation);
@Query("SELECT operation.*, message.uid FROM operation" +
" JOIN message ON message.id = operation.message" +
" WHERE folder = :folder" +
" ORDER BY operation.id")
List<TupleOperationEx> getOperations(long folder);
@Query("DELETE FROM operation WHERE id = :id")
void deleteOperation(long id);
@Query("DELETE FROM operation WHERE message = :id AND name = :name")
int deleteOperations(long id, String name);
}

View File

@@ -0,0 +1,64 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
@Entity(
tableName = EntityAccount.TABLE_NAME,
indices = {
}
)
public class EntityAccount {
static final String TABLE_NAME = "account";
@PrimaryKey(autoGenerate = true)
public Long id;
public String name;
@NonNull
public String host; // IMAP
@NonNull
public Integer port;
@NonNull
public String user;
@NonNull
public String password;
@NonNull
public Boolean primary;
@NonNull
public Boolean synchronize;
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityAccount) {
EntityAccount other = (EntityAccount) obj;
return (this.name == null ? other.name == null : this.name.equals(other.name) &&
this.host.equals(other.host) &&
this.port.equals(other.port) &&
this.user.equals(other.user) &&
this.password.equals(other.password) &&
this.primary.equals(other.primary) &&
this.synchronize.equals(other.synchronize));
} else
return false;
}
}

View File

@@ -0,0 +1,50 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import static android.arch.persistence.room.ForeignKey.CASCADE;
@Entity(
tableName = EntityAttachment.TABLE_NAME,
foreignKeys = {
@ForeignKey(childColumns = "message", entity = EntityMessage.class, parentColumns = "id", onDelete = CASCADE)
},
indices = {
@Index(value = {"message"})
}
)
public class EntityAttachment {
static final String TABLE_NAME = "attachment";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public Long message;
@NonNull
public String type;
public String name;
public byte[] content;
}

View File

@@ -0,0 +1,100 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import static android.arch.persistence.room.ForeignKey.CASCADE;
@Entity(
tableName = EntityFolder.TABLE_NAME,
foreignKeys = {
@ForeignKey(childColumns = "account", entity = EntityAccount.class, parentColumns = "id", onDelete = CASCADE)
},
indices = {
@Index(value = {"account", "name"}, unique = true),
@Index(value = {"account"}),
@Index(value = {"name"}),
@Index(value = {"type"})
}
)
public class EntityFolder {
static final String TABLE_NAME = "folder";
static final String TYPE_INBOX = "Inbox";
static final String TYPE_OUTBOX = "Outbox";
static final String TYPE_ARCHIVE = "All";
static final String TYPE_DRAFTS = "Drafts";
static final String TYPE_TRASH = "Trash";
static final String TYPE_JUNK = "Junk";
static final String TYPE_SENT = "Sent";
static final String TYPE_USER = "User";
static final List<String> STANDARD_FOLDER_ATTR = Arrays.asList(
"All",
"Drafts",
"Trash",
"Junk",
"Sent"
);
static final List<String> STANDARD_FOLDER_TYPE = Arrays.asList(
TYPE_ARCHIVE,
TYPE_DRAFTS,
TYPE_TRASH,
TYPE_JUNK,
TYPE_SENT
); // Must match STANDARD_FOLDER_ATTR
static boolean isOutgoing(String type) {
return (TYPE_OUTBOX.equals(type) || TYPE_DRAFTS.equals(type) || TYPE_SENT.equals(type));
}
@PrimaryKey(autoGenerate = true)
public Long id;
public Long account; // Outbox = null
@NonNull
public String name;
@NonNull
public String type;
@NonNull
public Boolean synchronize;
@NonNull
public Integer after; // days
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityFolder) {
EntityFolder other = (EntityFolder) obj;
return (this.account == null ? other.account == null : this.account.equals(other.account) &&
this.name.equals(other.name) &&
this.type.equals(other.type) &&
this.synchronize.equals(other.synchronize) &&
this.after.equals(other.after));
} else
return false;
}
}

View File

@@ -0,0 +1,78 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
@Entity(
tableName = EntityIdentity.TABLE_NAME,
indices = {
}
)
public class EntityIdentity {
static final String TABLE_NAME = "identity";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public String name;
@NonNull
public String email;
public String replyto;
@NonNull
public String host; // SMTP
@NonNull
public Integer port;
@NonNull
public Boolean starttls;
@NonNull
public String user;
@NonNull
public String password;
@NonNull
public Boolean primary;
@NonNull
public Boolean synchronize;
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityIdentity) {
EntityIdentity other = (EntityIdentity) obj;
return (this.name.equals(other.name) &&
this.email.equals(other.email) &&
this.replyto == null ? other.replyto == null : this.replyto.equals(other.replyto) &&
this.host.equals(other.host) &&
this.port.equals(other.port) &&
this.starttls.equals(other.starttls) &&
this.user.equals(other.user) &&
this.password.equals(other.password) &&
this.primary.equals(other.primary) &&
this.synchronize.equals(other.synchronize));
} else
return false;
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,111 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import static android.arch.persistence.room.ForeignKey.CASCADE;
// https://developer.android.com/training/data-storage/room/defining-data
@Entity(
tableName = EntityMessage.TABLE_NAME,
foreignKeys = {
@ForeignKey(childColumns = "account", entity = EntityAccount.class, parentColumns = "id", onDelete = CASCADE),
@ForeignKey(childColumns = "folder", entity = EntityFolder.class, parentColumns = "id", onDelete = CASCADE),
@ForeignKey(childColumns = "identity", entity = EntityIdentity.class, parentColumns = "id", onDelete = CASCADE),
@ForeignKey(childColumns = "replying", entity = EntityMessage.class, parentColumns = "id", onDelete = CASCADE)
},
indices = {
@Index(value = {"account"}),
@Index(value = {"folder"}),
@Index(value = {"identity"}),
@Index(value = {"replying"}),
@Index(value = {"folder", "uid"}, unique = true),
@Index(value = {"thread"}),
@Index(value = {"received"})
// ui_seen? ui_seen?
}
)
public class EntityMessage {
static final String TABLE_NAME = "message";
@PrimaryKey(autoGenerate = true)
public Long id;
public Long account; // performance, compose = null
@NonNull
public Long folder;
public Long identity;
public Long replying;
public Long uid; // compose = null
public String msgid;
public String references;
public String inreplyto;
public String thread; // compose = null
public String from;
public String to;
public String cc;
public String bcc;
public String reply;
public String subject;
public String body;
public Long sent; // compose = null
@NonNull
public Long received; // compose = stored
@NonNull
public Boolean seen;
@NonNull
public Boolean ui_seen;
@NonNull
public Boolean ui_hide;
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityMessage) {
EntityMessage other = (EntityMessage) obj;
return (this.account == null ? other.account == null : this.account.equals(other.account) &&
this.folder.equals(other.folder) &&
this.identity == null ? other.identity == null : this.identity.equals(other.identity) &&
this.replying == null ? other.replying == null : this.replying.equals(other.replying) &&
this.uid == null ? other.uid == null : this.uid.equals(other.uid) &&
this.msgid == null ? other.msgid == null : this.msgid.equals(other.msgid) &&
this.references == null ? other.references == null : this.references.equals(other.references) &&
this.inreplyto == null ? other.inreplyto == null : this.inreplyto.equals(other.inreplyto) &&
this.thread == null ? other.thread == null : thread.equals(other.thread) &&
this.from == null ? other.from == null : this.from.equals(other.from) &&
this.to == null ? other.to == null : this.to.equals(other.to) &&
this.cc == null ? other.cc == null : this.cc.equals(other.cc) &&
this.bcc == null ? other.bcc == null : this.bcc.equals(other.bcc) &&
this.reply == null ? other.reply == null : this.reply.equals(other.reply) &&
this.subject == null ? other.subject == null : this.subject.equals(other.subject) &&
this.body == null ? other.body == null : this.body.equals(other.body) &&
this.sent == null ? other.sent == null : this.sent.equals(other.sent) &&
this.received.equals(other.received) &&
this.seen.equals(other.seen) &&
this.ui_seen.equals(other.ui_seen) &&
this.ui_hide.equals(other.ui_hide));
}
return false;
}
}

View File

@@ -0,0 +1,95 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.json.JSONArray;
import static android.arch.persistence.room.ForeignKey.CASCADE;
@Entity(
tableName = EntityOperation.TABLE_NAME,
foreignKeys = {
@ForeignKey(childColumns = "message", entity = EntityMessage.class, parentColumns = "id", onDelete = CASCADE)
},
indices = {
@Index(value = {"message"})
}
)
public class EntityOperation {
static final String TABLE_NAME = "operation";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public Long message;
@NonNull
public String name;
public String args;
public static final String SEEN = "seen";
public static final String ADD = "add";
public static final String MOVE = "move";
public static final String DELETE = "delete";
public static final String SEND = "send";
static void queue(Context context, EntityMessage message, String name) {
JSONArray jsonArray = new JSONArray();
queue(context, message, name, jsonArray);
}
static void queue(Context context, EntityMessage message, String name, Object value) {
JSONArray jsonArray = new JSONArray();
jsonArray.put(value);
queue(context, message, name, jsonArray);
}
private static void queue(Context context, EntityMessage message, String name, JSONArray jsonArray) {
DaoOperation dao = DB.getInstance(context).operation();
int purged = 0;
if (SEEN.equals(name))
purged = dao.deleteOperations(message.id, name);
EntityOperation operation = new EntityOperation();
operation.message = message.id;
operation.name = name;
operation.args = jsonArray.toString();
operation.id = dao.insertOperation(operation);
Log.i(Helper.TAG, "Queued op=" + operation.id + "/" + name +
" args=" + operation.args +
" msg=" + message.folder + "/" + message.id + " uid=" + message.uid +
" purged=" + purged);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ServiceSynchronize.ACTION_PROCESS_OPERATIONS + message.folder));
}
}

View File

@@ -0,0 +1,319 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Toast;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.mail.Folder;
import javax.mail.MessagingException;
import javax.mail.Session;
public class FragmentAccount extends Fragment {
private List<Provider> providers;
private EditText etName;
private Spinner spProfile;
private EditText etHost;
private EditText etPort;
private EditText etUser;
private EditText etPassword;
private CheckBox cbPrimary;
private CheckBox cbSynchronize;
private Button btnOk;
private ProgressBar pbCheck;
static final int DEFAULT_INBOX_SYNC = 30;
static final int DEFAULT_STANDARD_SYNC = 7;
private static final List<String> standard_sync = Arrays.asList(
EntityFolder.TYPE_DRAFTS,
EntityFolder.TYPE_SENT
);
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_account, container, false);
// Get arguments
Bundle args = getArguments();
final long id = args.getLong("id", -1);
// Get providers
providers = Provider.loadProfiles(getContext());
providers.add(0, new Provider(getString(R.string.title_custom)));
// Get controls
spProfile = view.findViewById(R.id.spProvider);
etName = view.findViewById(R.id.etName);
etHost = view.findViewById(R.id.etHost);
etPort = view.findViewById(R.id.etPort);
etUser = view.findViewById(R.id.etUser);
etPassword = view.findViewById(R.id.etPassword);
cbPrimary = view.findViewById(R.id.cbPrimary);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
btnOk = view.findViewById(R.id.btnOk);
pbCheck = view.findViewById(R.id.pbCheck);
// Wire controls
spProfile.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Provider provider = providers.get(position);
if (provider.imap_port != 0) {
etName.setText(provider.name);
etHost.setText(provider.imap_host);
etPort.setText(Integer.toString(provider.imap_port));
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
ArrayAdapter<Provider> adapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item, providers);
adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
spProfile.setAdapter(adapter);
pbCheck.setVisibility(View.GONE);
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btnOk.setEnabled(false);
pbCheck.setVisibility(View.VISIBLE);
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("name", etName.getText().toString());
args.putString("host", etHost.getText().toString());
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", etPassword.getText().toString());
args.putBoolean("primary", cbPrimary.isChecked());
args.putBoolean("synchronize", cbSynchronize.isChecked());
getLoaderManager().restartLoader(ActivityView.LOADER_ACCOUNT_PUT, args, putLoaderCallbacks).forceLoad();
}
});
DB.getInstance(getContext()).account().liveAccount(id).observe(this, new Observer<EntityAccount>() {
@Override
public void onChanged(@Nullable EntityAccount account) {
etName.setText(account == null ? null : account.name);
etHost.setText(account == null ? null : account.host);
etPort.setText(account == null ? null : Long.toString(account.port));
etUser.setText(account == null ? null : account.user);
etPassword.setText(account == null ? null : account.password);
cbPrimary.setChecked(account == null ? true : account.primary);
cbSynchronize.setChecked(account == null ? true : account.synchronize);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_account);
}
private static class PutLoader extends AsyncTaskLoader<Throwable> {
private Bundle args;
PutLoader(Context context) {
super(context);
}
void setArgs(Bundle args) {
this.args = args;
}
@Override
public Throwable loadInBackground() {
try {
String name = args.getString("name");
String host = args.getString("host");
String port = args.getString("port");
String user = args.getString("user");
if (TextUtils.isEmpty(name))
name = host + "/" + user;
if (TextUtils.isEmpty(port))
port = "0";
DB db = DB.getInstance(getContext());
EntityAccount account = db.account().getAccount(args.getLong("id"));
boolean update = (account != null);
if (account == null)
account = new EntityAccount();
account.name = name;
account.host = host;
account.port = Integer.parseInt(port);
account.user = user;
account.password = Objects.requireNonNull(args.getString("password"));
account.primary = args.getBoolean("primary");
account.synchronize = args.getBoolean("synchronize");
// Check IMAP server
List<EntityFolder> folders = new ArrayList<>();
if (account.synchronize) {
Session isession = Session.getDefaultInstance(MessageHelper.getSessionProperties(), null);
IMAPStore istore = null;
try {
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(account.host, account.port, account.user, account.password);
if (!istore.hasCapability("IDLE"))
throw new MessagingException(getContext().getString(R.string.title_no_idle));
boolean drafts = false;
for (Folder ifolder : istore.getDefaultFolder().list("*")) {
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
for (String attr : attrs) {
if (attr.startsWith("\\")) {
int index = EntityFolder.STANDARD_FOLDER_ATTR.indexOf(attr.substring(1));
if (index >= 0) {
EntityFolder folder = new EntityFolder();
folder.name = ifolder.getFullName();
folder.type = EntityFolder.STANDARD_FOLDER_TYPE.get(index);
folder.synchronize = standard_sync.contains(folder.type);
folder.after = DEFAULT_STANDARD_SYNC;
folders.add(folder);
Log.i(Helper.TAG, "Standard folder=" + folder.name +
" type=" + folder.type + " attr=" + TextUtils.join(",", attrs));
if (EntityFolder.TYPE_DRAFTS.equals(folder.type))
drafts = true;
break;
}
}
}
}
if (!drafts)
throw new MessagingException(getContext().getString(R.string.title_no_drafts));
} finally {
if (istore != null)
istore.close();
}
}
if (account.primary)
db.account().resetPrimary();
if (update)
db.account().updateAccount(account);
else
try {
db.beginTransaction();
account.id = db.account().insertAccount(account);
EntityFolder inbox = new EntityFolder();
inbox.name = "INBOX";
inbox.type = EntityFolder.TYPE_INBOX;
inbox.synchronize = true;
inbox.after = DEFAULT_INBOX_SYNC;
folders.add(0, inbox);
for (EntityFolder folder : folders) {
folder.account = account.id;
Log.i(Helper.TAG, "Creating folder=" + folder.name + " (" + folder.type + ")");
folder.id = db.folder().insertFolder(folder);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
ServiceSynchronize.restart(getContext(), "account");
return null;
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return ex;
}
}
}
private LoaderManager.LoaderCallbacks putLoaderCallbacks = new LoaderManager.LoaderCallbacks<Throwable>() {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
PutLoader loader = new PutLoader(getActivity());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Throwable> loader, Throwable ex) {
getLoaderManager().destroyLoader(loader.getId());
btnOk.setEnabled(true);
pbCheck.setVisibility(View.GONE);
if (ex == null)
getFragmentManager().popBackStack();
else {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Throwable> loader) {
}
};
}

View File

@@ -0,0 +1,102 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import java.util.List;
public class FragmentAccounts extends Fragment {
private RecyclerView rvAccount;
private ProgressBar pbWait;
private Group grpReady;
private FloatingActionButton fab;
private AdapterAccount adapter;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_accounts, container, false);
// Get controls
rvAccount = view.findViewById(R.id.rvAccount);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab);
// Wire controls
rvAccount.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvAccount.setLayoutManager(llm);
adapter = new AdapterAccount(getContext());
rvAccount.setAdapter(adapter);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentAccount fragment = new FragmentAccount();
fragment.setArguments(new Bundle());
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
fragmentTransaction.commit();
}
});
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
// Observe accounts
DB.getInstance(getContext()).account().liveAccounts().observe(this, new Observer<List<EntityAccount>>() {
@Override
public void onChanged(@Nullable List<EntityAccount> accounts) {
adapter.set(accounts);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_list_accounts);
}
}

View File

@@ -0,0 +1,585 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.BottomNavigationView;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import static android.app.Activity.RESULT_OK;
public class FragmentCompose extends Fragment {
private boolean once = false;
private String thread = null;
private long rid = -1;
private Spinner spFrom;
private ImageView ivIdentyAdd;
private EditText etTo;
private ImageView ivContactAdd;
private EditText etSubject;
private EditText etBody;
private BottomNavigationView bottom_navigation;
private ProgressBar pbWait;
private Group grpReady;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_compose, container, false);
// Get arguments
Bundle args = getArguments();
String action = args.getString("action");
final long id = (TextUtils.isEmpty(action) ? args.getLong("id") : -1);
// Get controls
spFrom = view.findViewById(R.id.spFrom);
ivIdentyAdd = view.findViewById(R.id.ivIdentyAdd);
etTo = view.findViewById(R.id.etTo);
ivContactAdd = view.findViewById(R.id.ivContactAdd);
etSubject = view.findViewById(R.id.etSubject);
etBody = view.findViewById(R.id.etBody);
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
etBody.setMovementMethod(LinkMovementMethod.getInstance());
// Wire controls
ivIdentyAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle args = new Bundle();
args.putLong("id", -1);
FragmentIdentity fragment = new FragmentIdentity();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
fragmentTransaction.commit();
}
});
ivContactAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT);
}
});
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
bottom_navigation.setEnabled(false);
switch (item.getItemId()) {
case R.id.action_delete:
actionDelete(id);
return true;
case R.id.action_save:
actionPut(id, false);
return true;
case R.id.action_send:
actionPut(id, true);
return true;
}
return false;
}
});
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
bottom_navigation.getMenu().findItem(R.id.action_delete).setEnabled(id > 0);
bottom_navigation.setEnabled(false);
DB.getInstance(getContext()).identity().liveIdentities(true).observe(getActivity(), new Observer<List<EntityIdentity>>() {
@Override
public void onChanged(@Nullable final List<EntityIdentity> identities) {
Collections.sort(identities, new Comparator<EntityIdentity>() {
@Override
public int compare(EntityIdentity i1, EntityIdentity i2) {
return i1.name.compareTo(i2.name);
}
});
ArrayAdapter<EntityIdentity> adapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item, identities);
adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
spFrom.setAdapter(adapter);
// Select primary identity, also for saved drafts
for (int pos = 0; pos < identities.size(); pos++)
if (identities.get(pos).primary) {
spFrom.setSelection(pos);
break;
}
}
});
getLoaderManager().restartLoader(ActivityCompose.LOADER_COMPOSE_GET, getArguments(), getLoaderCallbacks).forceLoad();
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_compose);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ActivityCompose.REQUEST_CONTACT && resultCode == RESULT_OK) {
Cursor cursor = null;
try {
cursor = getContext().getContentResolver().query(data.getData(),
new String[]{
ContactsContract.CommonDataKinds.Email.ADDRESS,
ContactsContract.Contacts.DISPLAY_NAME
},
null, null, null);
if (cursor.moveToFirst()) {
int colEmail = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS);
int colName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
String email = cursor.getString(colEmail);
String name = cursor.getString(colName);
InternetAddress address = new InternetAddress(email, name);
StringBuilder sb = new StringBuilder(etTo.getText().toString());
if (sb.length() > 0)
sb.append("; ");
sb.append(address.toString());
etTo.setText(sb.toString());
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Toast.makeText(getContext(), ex.getMessage(), Toast.LENGTH_LONG).show();
} finally {
if (cursor != null)
cursor.close();
}
}
}
private void actionDelete(final long id) {
Bundle args = new Bundle();
args.putLong("id", id);
getLoaderManager().restartLoader(ActivityCompose.LOADER_COMPOSE_DELETE, args, deleteLoaderCallbacks).forceLoad();
}
private void actionPut(long id, boolean send) {
bottom_navigation.setEnabled(false);
EntityIdentity identity = (EntityIdentity) spFrom.getSelectedItem();
Bundle args = new Bundle();
args.putLong("id", id);
args.putLong("iid", identity == null ? -1 : identity.id);
args.putString("thread", FragmentCompose.this.thread);
args.putLong("rid", FragmentCompose.this.rid);
args.putString("to", etTo.getText().toString());
args.putString("subject", etSubject.getText().toString());
args.putString("body", etBody.getText().toString());
args.putBoolean("send", send);
getLoaderManager().restartLoader(ActivityCompose.LOADER_COMPOSE_PUT, args, putLoaderCallbacks).forceLoad();
}
private static class GetLoader extends AsyncTaskLoader<Bundle> {
private Bundle args;
GetLoader(Context context) {
super(context);
}
void setArgs(Bundle args) {
this.args = args;
}
@Nullable
@Override
public Bundle loadInBackground() {
String action = args.getString("action");
long id = args.getLong("id", -1);
EntityMessage msg = DB.getInstance(getContext()).message().getMessage(id);
Bundle result = new Bundle();
result.putString("action", action);
if (msg != null) {
if (msg.identity != null)
result.putLong("iid", msg.identity);
if (msg.replying != null)
result.putLong("rid", msg.replying);
result.putString("thread", msg.thread);
result.putString("subject", msg.subject);
result.putString("body", msg.body);
}
if (TextUtils.isEmpty(action)) {
if (msg != null) {
result.putString("from", msg.from);
result.putString("to", msg.to);
}
} else if ("reply".equals(action)) {
String to = null;
if (msg != null)
try {
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
to = (reply.length == 0 ? msg.from : msg.reply);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
result.putLong("rid", msg.id);
result.putString("from", msg.to);
result.putString("to", to);
} else if ("reply_all".equals(action)) {
String to = null;
if (msg != null) {
try {
Address[] from = MessageHelper.decodeAddresses(msg.from);
Address[] reply = MessageHelper.decodeAddresses(msg.reply);
Address[] cc = MessageHelper.decodeAddresses(msg.cc);
List<Address> addresses = new ArrayList<>();
addresses.addAll(Arrays.asList(reply.length == 0 ? from : reply));
addresses.addAll(Arrays.asList(cc));
to = MessageHelper.encodeAddresses(addresses.toArray(new Address[0]));
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
result.putLong("rid", msg.id);
result.putString("from", msg.to);
result.putString("to", to);
} else if ("forward".equals(action)) {
result.putString("from", msg.to);
result.putString("to", null);
}
return result;
}
}
private LoaderManager.LoaderCallbacks getLoaderCallbacks = new LoaderManager.LoaderCallbacks<Bundle>() {
@NonNull
@Override
public Loader<Bundle> onCreateLoader(int id, @Nullable Bundle args) {
GetLoader loader = new GetLoader(getActivity());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Bundle> loader, final Bundle result) {
getLoaderManager().destroyLoader(loader.getId());
long iid = result.getLong("iid", -1);
long rid = result.getLong("rid", -1);
String thread = result.getString("thread");
String from = result.getString("from");
String to = result.getString("to");
String subject = result.getString("subject");
String body = result.getString("body");
String action = result.getString("action");
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
FragmentCompose.this.thread = thread;
FragmentCompose.this.rid = rid;
ArrayAdapter adapter = (ArrayAdapter) spFrom.getAdapter();
if (adapter != null)
for (int pos = 0; pos < adapter.getCount(); pos++) {
EntityIdentity identity = (EntityIdentity) adapter.getItem(pos);
if (iid < 0 ? identity.primary : iid == identity.id) {
spFrom.setSelection(pos);
break;
}
}
if (!once) {
// Prevent changed fields from being overwritten
once = true;
if (action == null) {
if (to != null)
etTo.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(to)));
etSubject.setText(subject);
if (body != null)
etBody.setText(Html.fromHtml(HtmlHelper.sanitize(getContext(), body, false)));
} else if ("reply".equals(action) || "reply_all".equals(action)) {
etTo.setText(TextUtils.join(", ", MessageHelper.decodeAddresses(to)));
String text = String.format("<br><br>%s %s:<br><br>%s",
Html.escapeHtml(new Date().toString()),
Html.escapeHtml(TextUtils.join(", ", MessageHelper.decodeAddresses(from))),
HtmlHelper.sanitize(getContext(), body, true));
etSubject.setText(getContext().getString(R.string.title_subject_reply, subject));
etBody.setText(Html.fromHtml(text));
} else if ("forward".equals(action)) {
String text = String.format("<br><br>%s %s:<br><br>%s",
Html.escapeHtml(new Date().toString()),
Html.escapeHtml(TextUtils.join(", ", MessageHelper.decodeAddresses(from))),
HtmlHelper.sanitize(getContext(), body, true));
etSubject.setText(getContext().getString(R.string.title_subject_forward, subject));
etBody.setText(Html.fromHtml(text));
}
}
bottom_navigation.setEnabled(true);
}
@Override
public void onLoaderReset(@NonNull Loader<Bundle> loader) {
}
};
private static class DeleteLoader extends AsyncTaskLoader<Throwable> {
private Bundle args;
DeleteLoader(Context context) {
super(context);
}
void setArgs(Bundle args) {
this.args = args;
}
@Override
public Throwable loadInBackground() {
long id = args.getLong("id");
DaoMessage message = DB.getInstance(getContext()).message();
EntityMessage draft = message.getMessage(id);
if (draft != null) {
draft.ui_hide = true;
message.updateMessage(draft);
EntityOperation.queue(getContext(), draft, EntityOperation.DELETE);
}
return null;
}
}
private LoaderManager.LoaderCallbacks deleteLoaderCallbacks = new LoaderManager.LoaderCallbacks<Throwable>() {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, @Nullable Bundle args) {
DeleteLoader loader = new DeleteLoader(getActivity());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Throwable> loader, Throwable ex) {
getLoaderManager().destroyLoader(loader.getId());
if (ex == null) {
getFragmentManager().popBackStack();
Toast.makeText(getContext(), R.string.title_deleted, Toast.LENGTH_LONG).show();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Throwable> loader) {
}
};
private static class PutLoader extends AsyncTaskLoader<Throwable> {
private Bundle args;
PutLoader(Context context) {
super(context);
}
void setArgs(Bundle args) {
this.args = args;
}
@Override
public Throwable loadInBackground() {
long id = args.getLong("id");
boolean send = args.getBoolean("send", false);
Log.i(Helper.TAG, "Put load id=" + id + " send=" + send);
try {
DB db = DB.getInstance(getContext());
DaoMessage message = db.message();
DaoIdentity identity = db.identity();
DaoFolder folder = db.folder();
// Get data
EntityMessage draft = message.getMessage(id);
EntityIdentity ident = identity.getIdentity(args.getLong("iid"));
EntityFolder drafts = db.folder().getPrimaryDraftFolder();
if (drafts == null)
throw new Throwable(getContext().getString(R.string.title_no_primary_drafts));
long rid = args.getLong("rid", -1);
String thread = args.getString("thread");
String to = args.getString("to");
String body = args.getString("body");
String subject = args.getString("subject");
Address afrom = (ident == null ? null : new InternetAddress(ident.email, ident.name));
Address ato[] = (TextUtils.isEmpty(to) ? new Address[0] : InternetAddress.parse(to));
// Build draft
boolean update = (draft != null);
if (draft == null)
draft = new EntityMessage();
draft.account = drafts.account;
draft.folder = drafts.id;
draft.identity = (ident == null ? null : ident.id);
draft.replying = (rid < 0 ? null : rid);
draft.thread = thread;
draft.from = (afrom == null ? null : MessageHelper.encodeAddresses(new Address[]{afrom}));
draft.to = (ato == null ? null : MessageHelper.encodeAddresses(ato));
draft.subject = subject;
draft.body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.ui_hide = send;
// Store draft
if (update)
message.updateMessage(draft);
else
draft.id = message.insertMessage(draft);
// Check data
if (send) {
if (draft.identity == null)
throw new MessagingException(getContext().getString(R.string.title_from_missing));
if (draft.to == null)
throw new MessagingException(getContext().getString(R.string.title_to_missing));
// Get outbox
EntityFolder outbox = folder.getOutbox();
if (outbox == null) {
outbox = new EntityFolder();
outbox.name = "OUTBOX";
outbox.type = EntityFolder.TYPE_OUTBOX;
outbox.synchronize = false;
outbox.after = 0;
outbox.id = folder.insertFolder(outbox);
}
// Build outgoing message
EntityMessage out = new EntityMessage();
out.folder = outbox.id;
out.identity = draft.identity;
out.replying = draft.replying;
out.thread = draft.thread;
out.from = draft.from;
out.to = draft.to;
out.subject = draft.subject;
out.body = draft.body;
out.received = draft.received;
out.seen = draft.seen;
out.ui_seen = draft.ui_seen;
out.ui_hide = false;
out.id = message.insertMessage(out);
EntityOperation.queue(getContext(), out, EntityOperation.SEND);
EntityOperation.queue(getContext(), draft, EntityOperation.DELETE);
} else
EntityOperation.queue(getContext(), draft, EntityOperation.ADD);
return null;
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return ex;
}
}
}
private LoaderManager.LoaderCallbacks putLoaderCallbacks = new LoaderManager.LoaderCallbacks<Throwable>() {
private Bundle args;
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
this.args = args;
PutLoader loader = new PutLoader(getActivity());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Throwable> loader, Throwable ex) {
getLoaderManager().destroyLoader(loader.getId());
boolean send = args.getBoolean("send", false);
Log.i(Helper.TAG, "Put finished send=" + send + " ex=" + ex);
if (ex == null) {
getFragmentManager().popBackStack();
Toast.makeText(getContext(), send ? R.string.title_queued : R.string.title_saved, Toast.LENGTH_LONG).show();
} else {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Throwable> loader) {
}
};
}

View File

@@ -0,0 +1,60 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
public class FragmentEula extends Fragment {
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_eula, container, false);
Button btnAgree = view.findViewById(R.id.btnOk);
Button btnDisagree = view.findViewById(R.id.btnCancel);
btnAgree.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
prefs.edit().putBoolean("eula", true).apply();
}
});
btnDisagree.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getActivity().finish();
}
});
return view;
}
}

View File

@@ -0,0 +1,160 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
public class FragmentFolder extends Fragment {
private CheckBox cbSynchronize;
private EditText etAfter;
private Button btnOk;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_folder, container, false);
// Get arguments
Bundle args = getArguments();
final long id = args.getLong("id");
// Get controls
cbSynchronize = view.findViewById(R.id.cbSynchronize);
etAfter = view.findViewById(R.id.etAfter);
btnOk = view.findViewById(R.id.btnOk);
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btnOk.setEnabled(false);
Bundle args = new Bundle();
args.putLong("id", id);
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putString("after", etAfter.getText().toString());
getLoaderManager().restartLoader(ActivityView.LOADER_FOLDER_PUT, args, putLoaderCallbacks).forceLoad();
}
});
DB.getInstance(getContext()).folder().liveFolder(id).observe(this, new Observer<EntityFolder>() {
@Override
public void onChanged(@Nullable EntityFolder folder) {
if (folder != null) {
cbSynchronize.setChecked(folder.synchronize);
etAfter.setText(Integer.toString(folder.after));
}
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_folder);
}
private static class PutLoader extends AsyncTaskLoader<Throwable> {
private Bundle args;
PutLoader(Context context) {
super(context);
}
void setArgs(Bundle args) {
this.args = args;
}
@Override
public Throwable loadInBackground() {
try {
long id = args.getLong("id");
boolean synchronize = args.getBoolean("synchronize");
String after = args.getString("after");
int days = (TextUtils.isEmpty(after) ? 7 : Integer.parseInt(after));
DB db = DB.getInstance(getContext());
DaoFolder dao = db.folder();
EntityFolder folder = dao.getFolder(id);
folder.synchronize = synchronize;
folder.after = days;
dao.updateFolder(folder);
if (!folder.synchronize)
db.message().deleteMessages(folder.id);
ServiceSynchronize.restart(getContext(), "folder");
return null;
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return ex;
}
}
}
private LoaderManager.LoaderCallbacks putLoaderCallbacks = new LoaderManager.LoaderCallbacks<Throwable>() {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
PutLoader loader = new PutLoader(getActivity());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Throwable> loader, Throwable ex) {
getLoaderManager().destroyLoader(loader.getId());
btnOk.setEnabled(true);
if (ex == null)
getFragmentManager().popBackStack();
else {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Throwable> loader) {
}
};
}

View File

@@ -0,0 +1,97 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import java.util.List;
public class FragmentFolders extends Fragment {
private RecyclerView rvFolder;
private ProgressBar pbWait;
private Group grpReady;
private FloatingActionButton fab;
private AdapterFolder adapter;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_folders, container, false);
// Get controls
rvFolder = view.findViewById(R.id.rvFolder);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab);
// Wire controls
rvFolder.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvFolder.setLayoutManager(llm);
adapter = new AdapterFolder(getContext());
rvFolder.setAdapter(adapter);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
fab.setVisibility(View.GONE);
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
// Observe folders
DB.getInstance(getContext()).folder().liveFolders().observe(this, new Observer<List<TupleFolderEx>>() {
@Override
public void onChanged(@Nullable List<TupleFolderEx> folders) {
adapter.set(folders);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_list_folders);
}
}

View File

@@ -0,0 +1,102 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import java.util.List;
public class FragmentIdentities extends Fragment {
private RecyclerView rvIdentity;
private ProgressBar pbWait;
private Group grpReady;
private FloatingActionButton fab;
private AdapterIdentity adapter;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_identities, container, false);
// Get controls
rvIdentity = view.findViewById(R.id.rvIdentity);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab);
// Wire controls
rvIdentity.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvIdentity.setLayoutManager(llm);
adapter = new AdapterIdentity(getContext());
rvIdentity.setAdapter(adapter);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentIdentity fragment = new FragmentIdentity();
fragment.setArguments(new Bundle());
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
fragmentTransaction.commit();
}
});
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
// Observe identities
DB.getInstance(getContext()).identity().liveIdentities().observe(this, new Observer<List<EntityIdentity>>() {
@Override
public void onChanged(@Nullable List<EntityIdentity> identities) {
adapter.set(identities);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_list_identities);
}
}

View File

@@ -0,0 +1,289 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.Observer;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Toast;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import javax.mail.Session;
import javax.mail.Transport;
public class FragmentIdentity extends Fragment {
private List<Provider> providers;
private Spinner spProfile;
private EditText etName;
private EditText etEmail;
private EditText etHost;
private CheckBox cbStartTls;
private EditText etPort;
private EditText etUser;
private EditText etPassword;
private CheckBox cbPrimary;
private CheckBox cbSynchronize;
private Button btnOk;
private ProgressBar pbCheck;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_identity, container, false);
// Get arguments
Bundle args = getArguments();
final long id = args.getLong("id", -1);
// Get providers
providers = Provider.loadProfiles(getContext());
providers.add(0, new Provider(getString(R.string.title_custom)));
// Get controls
spProfile = view.findViewById(R.id.spProvider);
etName = view.findViewById(R.id.etName);
etEmail = view.findViewById(R.id.etEmail);
etHost = view.findViewById(R.id.etHost);
cbStartTls = view.findViewById(R.id.cbStartTls);
etPort = view.findViewById(R.id.etPort);
etUser = view.findViewById(R.id.etUser);
etPassword = view.findViewById(R.id.etPassword);
cbPrimary = view.findViewById(R.id.cbPrimary);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
btnOk = view.findViewById(R.id.btnOk);
pbCheck = view.findViewById(R.id.pbCheck);
// Wire controls
etEmail.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
etUser.setText(s.toString());
}
});
cbStartTls.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
etPort.setHint(checked ? "587" : "465");
}
});
spProfile.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Provider provider = providers.get(position);
if (provider.smtp_port != 0) {
etHost.setText(provider.smtp_host);
etPort.setText(Integer.toString(provider.smtp_port));
cbStartTls.setChecked(provider.starttls);
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
ArrayAdapter<Provider> adapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item, providers);
adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
spProfile.setAdapter(adapter);
pbCheck.setVisibility(View.GONE);
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btnOk.setEnabled(false);
pbCheck.setVisibility(View.VISIBLE);
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("name", etName.getText().toString());
args.putString("email", etEmail.getText().toString());
args.putString("host", etHost.getText().toString());
args.putBoolean("starttls", cbStartTls.isChecked());
args.putString("port", etPort.getText().toString());
args.putString("user", etUser.getText().toString());
args.putString("password", etPassword.getText().toString());
args.putBoolean("primary", cbPrimary.isChecked());
args.putBoolean("synchronize", cbSynchronize.isChecked());
getLoaderManager().restartLoader(ActivityView.LOADER_IDENTITY_PUT, args, putLoaderCallbacks).forceLoad();
}
});
DB.getInstance(getContext()).identity().liveIdentity(id).observe(this, new Observer<EntityIdentity>() {
@Override
public void onChanged(@Nullable EntityIdentity identity) {
etName.setText(identity == null ? null : identity.name);
etEmail.setText(identity == null ? null : identity.email);
etHost.setText(identity == null ? null : identity.host);
cbStartTls.setChecked(identity == null ? false : identity.starttls);
etPort.setText(identity == null ? null : Long.toString(identity.port));
etUser.setText(identity == null ? null : identity.user);
etPassword.setText(identity == null ? null : identity.password);
cbPrimary.setChecked(identity == null ? true : identity.primary);
cbSynchronize.setChecked(identity == null ? true : identity.synchronize);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_edit_indentity);
}
private static class PutLoader extends AsyncTaskLoader<Throwable> {
private Bundle args;
PutLoader(Context context) {
super(context);
}
void setArgs(Bundle args) {
this.args = args;
}
@Override
public Throwable loadInBackground() {
try {
long id = args.getLong("id");
String host = args.getString("host");
boolean starttls = args.getBoolean("starttls");
String port = args.getString("port");
if (TextUtils.isEmpty(port))
port = "0";
DB db = DB.getInstance(getContext());
EntityIdentity identity = db.identity().getIdentity(id);
boolean update = (identity != null);
if (identity == null)
identity = new EntityIdentity();
identity.name = Objects.requireNonNull(args.getString("name"));
identity.email = Objects.requireNonNull(args.getString("email"));
identity.host = host;
identity.port = Integer.parseInt(port);
identity.starttls = starttls;
identity.user = Objects.requireNonNull(args.getString("user"));
identity.password = Objects.requireNonNull(args.getString("password"));
identity.primary = args.getBoolean("primary");
identity.synchronize = args.getBoolean("synchronize");
if (TextUtils.isEmpty(identity.name))
throw new IllegalArgumentException(getContext().getString(R.string.title_no_name));
if (TextUtils.isEmpty(identity.email))
throw new IllegalArgumentException(getContext().getString(R.string.title_no_email));
// Check SMTP server
if (identity.synchronize) {
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getDefaultInstance(props, null);
Transport itransport = isession.getTransport(identity.starttls ? "smtp" : "smtps");
try {
itransport.connect(identity.host, identity.port, identity.user, identity.password);
} finally {
itransport.close();
}
}
if (identity.primary)
db.identity().resetPrimary();
if (update)
db.identity().updateIdentity(identity);
else
identity.id = db.identity().insertIdentity(identity);
return null;
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return ex;
}
}
}
private LoaderManager.LoaderCallbacks putLoaderCallbacks = new LoaderManager.LoaderCallbacks<Throwable>() {
@NonNull
@Override
public Loader<Throwable> onCreateLoader(int id, Bundle args) {
PutLoader loader = new PutLoader(getActivity());
loader.setArgs(args);
return loader;
}
@Override
public void onLoadFinished(@NonNull Loader<Throwable> loader, Throwable ex) {
getLoaderManager().destroyLoader(loader.getId());
btnOk.setEnabled(true);
pbCheck.setVisibility(View.GONE);
if (ex == null)
getFragmentManager().popBackStack();
else {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Toast.makeText(getContext(), Helper.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Throwable> loader) {
}
};
}

View File

@@ -0,0 +1,329 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.BottomNavigationView;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FragmentMessage extends Fragment {
private TextView tvTime;
private TextView tvFrom;
private TextView tvSubject;
private TextView tvCount;
private BottomNavigationView top_navigation;
private TextView tvBody;
private BottomNavigationView bottom_navigation;
private ProgressBar pbWait;
private Group grpReady;
private LiveData<TupleFolderEx> liveFolder;
private ExecutorService executor = Executors.newCachedThreadPool();
private DateFormat df = SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_message, container, false);
// Get arguments
Bundle args = getArguments();
final long folder = args.getLong("folder");
final long id = args.getLong("id");
// Get controls
tvFrom = view.findViewById(R.id.tvAddress);
tvTime = view.findViewById(R.id.tvTime);
tvSubject = view.findViewById(R.id.tvSubject);
tvCount = view.findViewById(R.id.tvCount);
top_navigation = view.findViewById(R.id.top_navigation);
tvBody = view.findViewById(R.id.tvBody);
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
tvTime.setTextIsSelectable(true);
tvFrom.setTextIsSelectable(true);
tvSubject.setTextIsSelectable(true);
tvBody.setTextIsSelectable(true);
tvBody.setMovementMethod(LinkMovementMethod.getInstance());
// Wire controls
top_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_seen:
onActionSeen(id);
return true;
case R.id.action_thread:
onActionThread(id);
return true;
case R.id.action_move:
onActionMove(id);
return true;
case R.id.action_forward:
onActionForward(id);
return true;
case R.id.action_reply_all:
onActionReplyAll(id);
return true;
}
return false;
}
});
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_delete:
onActionDelete(id);
return true;
case R.id.action_spam:
onActionSpam(id);
return true;
case R.id.action_archive:
onActionArchive(id);
return true;
case R.id.action_reply:
onActionReply(id);
return true;
}
return false;
}
});
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
DB db = DB.getInstance(getContext());
// Observe folder
liveFolder = db.folder().liveFolderEx(folder);
// Observe message
db.message().liveMessage(id).observe(this, new Observer<TupleMessageEx>() {
@Override
public void onChanged(@Nullable TupleMessageEx message) {
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
if (message == null || message.ui_hide) {
// Message gone (moved, deleted)
if (FragmentMessage.this.isVisible())
getFragmentManager().popBackStack();
} else {
tvFrom.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from));
tvTime.setText(message.sent == null ? null : df.format(new Date(message.sent)));
tvSubject.setText(message.subject);
tvCount.setText(Integer.toString(message.count));
tvCount.setVisibility(message.count > 1 ? View.VISIBLE : View.GONE);
int visibility = (message.ui_seen ? Typeface.NORMAL : Typeface.BOLD);
tvFrom.setTypeface(null, visibility);
tvTime.setTypeface(null, visibility);
tvSubject.setTypeface(null, visibility);
tvCount.setTypeface(null, visibility);
MenuItem actionSeen = top_navigation.getMenu().findItem(R.id.action_seen);
actionSeen.setIcon(message.ui_seen
? R.drawable.baseline_visibility_off_24
: R.drawable.baseline_visibility_24);
actionSeen.setTitle(message.ui_seen ? R.string.title_unseen : R.string.title_seen);
tvBody.setText(message.body == null
? null
: Html.fromHtml(HtmlHelper.sanitize(getContext(), message.body, false)));
}
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
liveFolder.observe(this, folderObserver);
}
@Override
public void onPause() {
super.onPause();
liveFolder.removeObservers(this);
}
Observer<TupleFolderEx> folderObserver = new Observer<TupleFolderEx>() {
@Override
public void onChanged(@Nullable TupleFolderEx folder) {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(folder == null
? null
: Helper.localizeFolderName(getContext(), folder));
}
};
private void onActionSeen(final long id) {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
message.ui_seen = !message.ui_seen;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.SEEN, message.ui_seen);
}
});
}
private void onActionThread(long id) {
FragmentMessages fragment = new FragmentMessages();
Bundle args = new Bundle();
args.putLong("thread", id); // message ID
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("thread");
fragmentTransaction.commit();
}
private void onActionMove(final long id) {
Toast.makeText(getContext(), "Not implemented yet", Toast.LENGTH_LONG).show();
}
private void onActionForward(long id) {
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("id", id)
.putExtra("action", "forward"));
}
private void onActionReplyAll(long id) {
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("id", id)
.putExtra("action", "reply_all"));
}
private void onActionDelete(final long id) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder
.setMessage(R.string.title_ask_delete)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.DELETE);
}
});
}
})
.setNegativeButton(android.R.string.cancel, null).show();
}
private void onActionSpam(final long id) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder
.setMessage(R.string.title_ask_spam)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
EntityFolder spam = db.folder().getSpamFolder(message.account);
if (spam == null) {
Toast.makeText(getContext(), R.string.title_no_spam, Toast.LENGTH_LONG).show();
return;
}
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, spam.id);
}
});
}
})
.setNegativeButton(android.R.string.cancel, null).show();
}
private void onActionArchive(final long id) {
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(getContext());
EntityMessage message = db.message().getMessage(id);
EntityFolder archive = db.folder().getArchiveFolder(message.account);
if (archive == null) {
Toast.makeText(getContext(), R.string.title_no_archive, Toast.LENGTH_LONG).show();
return;
}
message.ui_hide = true;
db.message().updateMessage(message);
EntityOperation.queue(getContext(), message, EntityOperation.MOVE, archive.id);
}
});
}
private void onActionReply(long id) {
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("id", id)
.putExtra("action", "reply"));
}
}

View File

@@ -0,0 +1,152 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.Group;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
public class FragmentMessages extends Fragment {
private RecyclerView rvMessage;
private TextView tvNoEmail;
private ProgressBar pbWait;
private Group grpReady;
private FloatingActionButton fab;
private AdapterMessage adapter;
private LiveData<TupleFolderEx> liveFolder;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_messages, container, false);
// Get arguments
long folder = getArguments().getLong("folder", -1);
long thread = getArguments().getLong("thread", -1); // message ID
// Get controls
rvMessage = view.findViewById(R.id.rvFolder);
tvNoEmail = view.findViewById(R.id.tvNoEmail);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab);
// Wire controls
rvMessage.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvMessage.setLayoutManager(llm);
adapter = new AdapterMessage(getContext(),
thread < 0
? AdapterMessage.ViewType.FOLDER
: AdapterMessage.ViewType.THREAD);
rvMessage.setAdapter(adapter);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(getContext(), ActivityCompose.class));
}
});
// Initialize
tvNoEmail.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
DB db = DB.getInstance(getContext());
// Observe folder
liveFolder = (thread < 0 ? DB.getInstance(getContext()).folder().liveFolderEx(folder) : null);
// Observe messages
if (thread < 0)
if (folder < 0)
db.message().liveUnifiedInbox().observe(this, messagesObserver);
else
db.message().liveMessages(folder).observe(this, messagesObserver);
else {
db.message().liveThread(thread).observe(this, messagesObserver);
}
return view;
}
@Override
public void onResume() {
super.onResume();
if (liveFolder == null)
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_folder_thread);
else
liveFolder.observe(this, folderObserver);
}
@Override
public void onPause() {
super.onPause();
if (liveFolder != null)
liveFolder.removeObservers(this);
}
Observer<TupleFolderEx> folderObserver = new Observer<TupleFolderEx>() {
@Override
public void onChanged(@Nullable TupleFolderEx folder) {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(folder.name == null
? getString(R.string.title_folder_unified)
: Helper.localizeFolderName(getContext(), folder));
}
};
Observer<List<TupleMessageEx>> messagesObserver = new Observer<List<TupleMessageEx>>() {
@Override
public void onChanged(@Nullable List<TupleMessageEx> messages) {
adapter.set(messages);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
if (messages.size() == 0) {
tvNoEmail.setVisibility(View.VISIBLE);
rvMessage.setVisibility(View.GONE);
} else {
tvNoEmail.setVisibility(View.GONE);
rvMessage.setVisibility(View.VISIBLE);
}
}
};
}

View File

@@ -0,0 +1,192 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.Manifest;
import android.arch.lifecycle.Observer;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
public class FragmentSetup extends Fragment {
private Button btnAccount;
private Button btnIdentity;
private Button btnPermissions;
private ProgressBar pbAccount;
private ProgressBar pbIdentity;
private TextView tvAccountDone;
private TextView tvIdentityDone;
private TextView tvPermissionsDone;
private static final String[] permissions = new String[]{
Manifest.permission.READ_CONTACTS
};
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_setup, container, false);
// Get controls
btnAccount = view.findViewById(R.id.btnAccount);
btnIdentity = view.findViewById(R.id.btnIdentity);
btnPermissions = view.findViewById(R.id.btnPermissions);
pbAccount = view.findViewById(R.id.pbAccount);
pbIdentity = view.findViewById(R.id.pbIdentity);
tvAccountDone = view.findViewById(R.id.tvAccountDone);
tvIdentityDone = view.findViewById(R.id.tvIdentityDone);
tvPermissionsDone = view.findViewById(R.id.tvPermissionsDone);
// Wire controls
btnAccount.setOnClickListener(new View.OnClickListener() {
private boolean once;
@Override
public void onClick(View view) {
once = false;
btnAccount.setEnabled(false);
pbAccount.setVisibility(View.VISIBLE);
DB.getInstance(getContext()).account().liveFirstAccount().observe(getActivity(), new Observer<EntityAccount>() {
@Override
public void onChanged(@Nullable EntityAccount account) {
btnAccount.setEnabled(true);
pbAccount.setVisibility(View.GONE);
if (!once) {
once = true;
Bundle args = new Bundle();
if (account != null)
args.putLong("id", account.id);
FragmentAccount fragment = new FragmentAccount();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account");
fragmentTransaction.commit();
}
}
});
}
});
btnIdentity.setOnClickListener(new View.OnClickListener() {
private boolean once;
@Override
public void onClick(View view) {
once = false;
btnIdentity.setEnabled(false);
pbIdentity.setVisibility(View.VISIBLE);
DB.getInstance(getContext()).identity().liveFirstIdentity().observe(getActivity(), new Observer<EntityIdentity>() {
@Override
public void onChanged(@Nullable EntityIdentity identity) {
btnIdentity.setEnabled(true);
pbIdentity.setVisibility(View.GONE);
if (!once) {
once = true;
Bundle args = new Bundle();
if (identity != null)
args.putLong("id", identity.id);
FragmentIdentity fragment = new FragmentIdentity();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
fragmentTransaction.commit();
}
}
});
}
});
btnPermissions.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestPermissions(permissions, 1);
}
});
// Initialize
pbAccount.setVisibility(View.GONE);
pbIdentity.setVisibility(View.GONE);
tvAccountDone.setVisibility(View.INVISIBLE);
tvIdentityDone.setVisibility(View.INVISIBLE);
tvPermissionsDone.setVisibility(View.INVISIBLE);
DB db = DB.getInstance(getContext());
db.account().liveAccounts(true).observe(this, new Observer<List<EntityAccount>>() {
@Override
public void onChanged(@Nullable List<EntityAccount> accounts) {
tvAccountDone.setVisibility(accounts.size() > 0 ? View.VISIBLE : View.INVISIBLE);
}
});
db.identity().liveIdentities(true).observe(this, new Observer<List<EntityIdentity>>() {
@Override
public void onChanged(@Nullable List<EntityIdentity> identities) {
tvIdentityDone.setVisibility(identities.size() > 0 ? View.VISIBLE : View.INVISIBLE);
}
});
int[] grantResults = new int[permissions.length];
for (int i = 0; i < permissions.length; i++)
grantResults[i] = ContextCompat.checkSelfPermission(getActivity(), permissions[i]);
onRequestPermissionsResult(0, permissions, grantResults);
return view;
}
@Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.title_setup);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean has = (grantResults.length > 0);
for (int result : grantResults)
if (result != PackageManager.PERMISSION_GRANTED) {
has = false;
break;
}
btnPermissions.setEnabled(!has);
tvPermissionsDone.setVisibility(has ? View.VISIBLE : View.INVISIBLE);
}
}

View File

@@ -0,0 +1,63 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import android.util.TypedValue;
public class Helper {
static final String TAG = BuildConfig.APPLICATION_ID;
static int resolveColor(Context context, int attr) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(attr, typedValue, true);
return typedValue.data;
}
static String localizeFolderName(Context context, String name) {
if ("INBOX".equals(name))
return context.getString(R.string.title_folder_inbox);
else if ("OUTBOX".equals(name))
return context.getString(R.string.title_folder_outbox);
else
return name;
}
static String localizeFolderName(Context context, TupleFolderEx folder) {
if (TextUtils.isEmpty(folder.accountName))
return localizeFolderName(context, folder.name);
else
return localizeFolderName(context, folder.name) + "/" + folder.accountName;
}
static String formatThrowable(Throwable ex) {
StringBuilder sb = new StringBuilder();
sb.append(ex.getMessage());
Throwable cause = ex.getCause();
while (cause != null) {
sb.append(" ").append(cause.getMessage());
cause = cause.getCause();
}
return sb.toString();
}
}

View File

@@ -0,0 +1,95 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.text.TextUtils;
import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.util.ArrayList;
import java.util.List;
public class HtmlHelper implements NodeVisitor {
private Context context;
private String newline;
private List<String> links = new ArrayList<>();
private StringBuilder sb = new StringBuilder();
private HtmlHelper(Context context, boolean reply) {
this.context = context;
this.newline = (reply ? "<br>> " : "<br>");
}
public void head(Node node, int depth) {
String name = node.nodeName();
if (node instanceof TextNode)
sb.append(((TextNode) node).text());
else if (name.equals("li"))
sb.append(newline).append(" * ");
else if (name.equals("dt"))
sb.append(" ");
else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr", "div"))
sb.append(newline);
}
public void tail(Node node, int depth) {
String name = node.nodeName();
if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5", "div"))
sb.append(newline);
else if (name.equals("a")) {
String link = node.absUrl("href");
if (!TextUtils.isEmpty(link)) {
if (!links.contains(link))
links.add(link);
sb.append(" ").append(context.getString(R.string.title_link, link, links.size()));
}
} else if (name.equals("img")) {
String link = node.absUrl("src");
if (!TextUtils.isEmpty(link)) {
if (!links.contains(link))
links.add(link);
sb.append(" ").append(context.getString(R.string.title_image, link, links.size()));
}
}
}
@Override
public String toString() {
if (links.size() > 0)
sb.append(newline).append(newline);
for (int i = 0; i < links.size(); i++)
sb.append(String.format("[%d] %s ", i + 1, links.get(i))).append(newline);
return sb.toString();
}
public static String sanitize(Context context, String html, boolean reply) {
Document document = Jsoup.parse(html);
HtmlHelper visitor = new HtmlHelper(context, reply);
NodeTraversor.traverse(visitor, document.body());
return visitor.toString();
}
}

View File

@@ -0,0 +1,282 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import javax.mail.Address;
import javax.mail.Flags;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
public class MessageHelper {
private MimeMessage imessage;
private String raw = null;
static Properties getSessionProperties() {
Properties props = new Properties();
// https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
props.put("mail.imaps.ssl.checkserveridentity", "true");
props.put("mail.imaps.ssl.trust", "*");
props.put("mail.imaps.starttls.enable", "false");
props.put("mail.imaps.timeout", "20000");
props.put("mail.imaps.connectiontimeout", "20000");
// https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
props.put("mail.smtps.ssl.checkserveridentity", "true");
props.put("mail.smtps.ssl.trust", "*");
props.put("mail.smtps.starttls.enable", "false");
props.put("mail.smtps.starttls.required", "false");
props.put("mail.smtps.auth", "true");
props.put("mail.smtps.timeout", "20000");
props.put("mail.smtps.connectiontimeout", "20000");
props.put("mail.smtp.ssl.checkserveridentity", "true");
props.put("mail.smtp.ssl.trust", "*");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "true");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.timeout", "20000");
props.put("mail.smtp.connectiontimeout", "20000");
return props;
}
static MimeMessage from(EntityMessage message, Session isession) throws MessagingException {
MimeMessage imessage = new MimeMessage(isession);
if (message.from != null)
imessage.setFrom(MessageHelper.decodeAddresses(message.from)[0]);
if (message.to != null)
imessage.setRecipients(Message.RecipientType.TO, MessageHelper.decodeAddresses(message.to));
if (message.cc != null)
imessage.setRecipients(Message.RecipientType.CC, MessageHelper.decodeAddresses(message.to));
if (message.subject != null)
imessage.setSubject(message.subject);
if (message.body != null)
imessage.setText(message.body, null, "html");
imessage.setSentDate(new Date());
return imessage;
}
static MimeMessage from(EntityMessage message, EntityMessage reply, Session isession) throws MessagingException {
MimeMessage imessage = from(message, isession);
imessage.addHeader("In-Reply-To", reply.msgid);
imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid);
return imessage;
}
MessageHelper(MimeMessage message) {
this.imessage = message;
}
MessageHelper(String raw, Session isession) throws MessagingException {
byte[] bytes = Base64.decode(raw, Base64.URL_SAFE);
InputStream is = new ByteArrayInputStream(bytes);
this.imessage = new MimeMessage(isession, is);
}
String getMessageID() throws MessagingException {
return imessage.getHeader("Message-ID", null);
}
String[] getReferences() throws MessagingException {
String refs = imessage.getHeader("References", null);
return (refs == null ? new String[0] : refs.split("\\s+"));
}
String getInReplyTo() throws MessagingException {
return imessage.getHeader("In-Reply-To", null);
}
String getThreadId(long uid) throws MessagingException {
for (String ref : getReferences())
if (!TextUtils.isEmpty(ref))
return ref;
String msgid = getMessageID();
return (TextUtils.isEmpty(msgid) ? Long.toString(uid) : msgid);
}
String getFrom() throws MessagingException, JSONException {
return encodeAddresses(imessage.getFrom());
}
String getTo() throws MessagingException, JSONException {
return encodeAddresses(imessage.getRecipients(Message.RecipientType.TO));
}
String getCc() throws MessagingException, JSONException {
return encodeAddresses(imessage.getRecipients(Message.RecipientType.CC));
}
String getReply() throws MessagingException, JSONException {
return encodeAddresses(imessage.getReplyTo());
}
static String encodeAddresses(Address[] addresses) throws JSONException {
JSONArray jaddresses = new JSONArray();
if (addresses != null)
for (Address address : addresses)
if (address instanceof InternetAddress) {
String a = ((InternetAddress) address).getAddress();
String p = ((InternetAddress) address).getPersonal();
JSONObject jaddress = new JSONObject();
if (a != null)
jaddress.put("address", a);
if (p != null)
jaddress.put("personal", p);
jaddresses.put(jaddress);
}
return jaddresses.toString();
}
static Address[] decodeAddresses(String json) {
List<Address> result = new ArrayList<>();
try {
JSONArray jaddresses = new JSONArray(json);
for (int i = 0; i < jaddresses.length(); i++) {
JSONObject jaddress = (JSONObject) jaddresses.get(i);
if (jaddress.has("personal"))
result.add(new InternetAddress(
jaddress.getString("address"),
jaddress.getString("personal")));
else
result.add(new InternetAddress(
jaddress.getString("address")));
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
return result.toArray(new Address[0]);
}
String getHtml() throws MessagingException {
return getHtml(imessage);
}
static String getFormattedAddresses(String json) {
try {
List<String> addresses = new ArrayList<>();
for (Address address : decodeAddresses(json))
if (address instanceof InternetAddress) {
InternetAddress a = (InternetAddress) address;
String personal = a.getPersonal();
if (TextUtils.isEmpty(personal))
addresses.add(address.toString());
else
addresses.add(personal);
} else
addresses.add(address.toString());
return TextUtils.join(", ", addresses);
} catch (Throwable ex) {
return ex.getMessage();
}
}
private String getHtml(Part part) throws MessagingException {
if (part.isMimeType("text/*"))
try {
String s = (String) part.getContent();
if (part.isMimeType("text/plain"))
s = "<pre>" + s.replaceAll("\\r?\\n", "<br />") + "</pre>";
return s;
} catch (IOException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return null;
}
if (part.isMimeType("multipart/alternative")) {
String text = null;
try {
Multipart mp = (Multipart) part.getContent();
for (int i = 0; i < mp.getCount(); i++) {
Part bp = mp.getBodyPart(i);
if (bp.isMimeType("text/plain")) {
if (text == null)
text = getHtml(bp);
} else if (bp.isMimeType("text/html")) {
String s = getHtml(bp);
if (s != null)
return s;
} else
return getHtml(bp);
}
} catch (IOException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
return text;
}
if (part.isMimeType("multipart/*")) {
try {
Multipart mp = (Multipart) part.getContent();
for (int i = 0; i < mp.getCount(); i++) {
String s = getHtml(mp.getBodyPart(i));
if (s != null)
return s;
}
} catch (IOException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
return null;
}
boolean getSeen() throws MessagingException {
return imessage.isSet(Flags.Flag.SEEN);
}
String getRaw() throws IOException, MessagingException {
if (raw == null) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
imessage.writeTo(os);
raw = Base64.encodeToString(os.toByteArray(), Base64.URL_SAFE);
}
return raw;
}
}

View File

@@ -0,0 +1,87 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import java.util.ArrayList;
import java.util.List;
public class Provider {
public String name;
public String imap_host;
public int imap_port;
public String smtp_host;
public int smtp_port;
public boolean starttls;
private Provider() {
}
Provider(String name) {
this.name = name;
}
static List<Provider> loadProfiles(Context context) {
List<Provider> result = null;
try {
XmlResourceParser xml = context.getResources().getXml(R.xml.providers);
int eventType = xml.getEventType();
Provider provider = null;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
if ("providers".equals(xml.getName()))
result = new ArrayList<>();
else if ("provider".equals(xml.getName())) {
provider = new Provider();
provider.name = xml.getAttributeValue(null, "name");
} else if ("imap".equals(xml.getName())) {
provider.imap_host = xml.getAttributeValue(null, "host");
provider.imap_port = xml.getAttributeIntValue(null, "port", 0);
} else if ("smtp".equals(xml.getName())) {
provider.smtp_host = xml.getAttributeValue(null, "host");
provider.smtp_port = xml.getAttributeIntValue(null, "port", 0);
provider.starttls = xml.getAttributeBooleanValue(null, "starttls", false);
} else
throw new IllegalAccessException(xml.getName());
} else if (eventType == XmlPullParser.END_TAG) {
if ("provider".equals(xml.getName())) {
result.add(provider);
provider = null;
}
}
eventType = xml.next();
}
} catch (Throwable ex) {
Log.e(Helper.TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
}
return result;
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,33 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class ReceiverAutostart extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()))
ServiceSynchronize.start(context);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
public class TupleAccountStats {
public Integer accounts;
public Integer operations;
}

View File

@@ -0,0 +1,38 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
public class TupleFolderEx extends EntityFolder {
public String accountName;
public int messages;
public int unseen;
@Override
public boolean equals(Object obj) {
if (obj instanceof TupleFolderEx) {
TupleFolderEx other = (TupleFolderEx) obj;
return (super.equals(obj) &&
this.accountName == null ? other.accountName == null : accountName.equals(other.accountName) &&
this.messages == other.messages &&
this.unseen == other.unseen);
} else
return false;
}
}

View File

@@ -0,0 +1,39 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
public class TupleMessageEx extends EntityMessage {
public String folderName;
public String folderType;
public int count;
public int unseen;
@Override
public boolean equals(Object obj) {
if (obj instanceof TupleMessageEx) {
TupleMessageEx other = (TupleMessageEx) obj;
return (super.equals(obj) &&
this.folderType.equals(other.folderType) &&
this.count == other.count &&
this.unseen == other.unseen);
}
return super.equals(obj);
}
}

View File

@@ -0,0 +1,24 @@
package eu.faircode.email;
/*
This file is part of Safe email.
Safe email 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.
NetGuard 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 NetGuard. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018 by Marcel Bokhorst (M66B)
*/
public class TupleOperationEx extends EntityOperation {
public Long uid;
}