├── forward-cat-website ├── project │ ├── build.properties │ └── plugins.sbt ├── README ├── public │ └── images │ │ ├── favicon.ico │ │ └── forward.png ├── conf │ ├── quartz-scheduler.properties │ ├── ebean.properties │ ├── logger.xml │ ├── application.conf │ ├── routes │ ├── messages.en │ ├── messages.ca │ └── messages.es ├── app │ ├── views │ │ ├── contact_email.scala.html │ │ ├── user_reported_email.scala.html │ │ ├── contact_sent.scala.html │ │ ├── user_reported.scala.html │ │ ├── email_sent.scala.html │ │ ├── proxy_deleted.scala.html │ │ ├── proxy_created.scala.html │ │ ├── proxy_extended.scala.html │ │ ├── confirm_deletion.scala.html │ │ ├── proxy_expiring_email.scala.html │ │ ├── error_page.scala.html │ │ ├── proxy_created_email.scala.html │ │ ├── stats.scala.html │ │ ├── button_email.scala.html │ │ ├── contact.scala.html │ │ ├── report.scala.html │ │ ├── navbar.scala.html │ │ ├── simple_page.scala.html │ │ ├── faq.scala.html │ │ ├── simple_email.scala.html │ │ └── forward.scala.html │ ├── controllers │ │ ├── Faq.java │ │ ├── RedirectAction.java │ │ ├── Landing.java │ │ ├── Stats.java │ │ ├── ValidateProxy.java │ │ ├── Contact.java │ │ ├── Report.java │ │ ├── ConfirmProxy.java │ │ ├── ExtendProxy.java │ │ ├── DeleteProxy.java │ │ └── AddProxy.java │ ├── models │ │ ├── JedisHelper.java │ │ ├── MailAddressNormalizer.java │ │ ├── SpamCatcher.java │ │ ├── MailSender.java │ │ ├── ExpirationUtils.java │ │ ├── Options.java │ │ ├── DomainManager.java │ │ ├── ControllerUtils.java │ │ ├── Repository.java │ │ └── StatsRepository.java │ ├── alerts │ │ ├── GuiceJobFactory.java │ │ └── SendAlertJob.java │ ├── Global.java │ ├── Module.java │ └── assets │ │ ├── stylesheets │ │ └── styles.less │ │ └── javascripts │ │ └── main.js ├── default-drop.sql ├── indexes.sql ├── test │ ├── controllers │ │ ├── ContainsHtmlLang.java │ │ ├── FaqTest.java │ │ ├── LandingTest.java │ │ ├── PlayTest.java │ │ ├── ContactTest.java │ │ ├── TestUtils.java │ │ ├── ExtendProxyTest.java │ │ ├── ValidateProxyTest.java │ │ ├── ConfirmDeletionTest.java │ │ ├── ConfirmProxyTest.java │ │ ├── DeleteProxyTest.java │ │ ├── RedirectActionTest.java │ │ ├── ReportTest.java │ │ └── AddProxyTest.java │ ├── models │ │ ├── MailAddressNormalizerTest.java │ │ ├── DomainManagerTest.java │ │ ├── ControllerUtilsTest.java │ │ ├── SpamCatcherTest.java │ │ └── TestHttpRequest.java │ └── alerts │ │ └── SendAlertJobTest.java ├── default-create.sql └── build.sbt ├── .gitignore └── forward-cat ├── forward-cat-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── forwardcat │ │ └── common │ │ ├── StringNormalizer.java │ │ ├── RedisKeys.java │ │ ├── User.java │ │ └── ProxyMail.java └── pom.xml ├── forward-cat-james ├── src │ ├── main │ │ ├── resources │ │ │ ├── forward-cat.properties │ │ │ ├── lmtpserver.xml │ │ │ ├── imapserver.xml │ │ │ ├── pop3server.xml │ │ │ └── domainlist.xml │ │ └── java │ │ │ └── com │ │ │ └── forwardcat │ │ │ └── james │ │ │ ├── MailUtils.java │ │ │ ├── FileDKIMSign.java │ │ │ └── ForwardCatResourcesProvider.java │ └── test │ │ └── java │ │ └── com │ │ └── forwardcat │ │ └── james │ │ ├── FileDKIMSignTest.java │ │ └── ForwardMailetTest.java └── pom.xml ├── README.md └── pom.xml /forward-cat-website/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /forward-cat-website/README: -------------------------------------------------------------------------------- 1 | Forward Cat Server 2 | ===================================== 3 | 4 | This is the web server of forward.cat 5 | -------------------------------------------------------------------------------- /forward-cat-website/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danisola/forward-cat/HEAD/forward-cat-website/public/images/favicon.ico -------------------------------------------------------------------------------- /forward-cat-website/public/images/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danisola/forward-cat/HEAD/forward-cat-website/public/images/forward.png -------------------------------------------------------------------------------- /forward-cat-website/conf/quartz-scheduler.properties: -------------------------------------------------------------------------------- 1 | org.quartz.scheduler.instanceName = AlertSenderQuartzScheduler 2 | org.quartz.threadPool.threadCount = 1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | .idea 9 | *.iml 10 | out 11 | .idea_modules 12 | .classpath 13 | .project 14 | RUNNING_PID 15 | .settings 16 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/contact_email.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, email: String, message: String) 2 | 3 | @simple_email(lang, "Contact") { 4 |

From: @email

5 |

Message:

6 |

@message

7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/default-drop.sql: -------------------------------------------------------------------------------- 1 | SET REFERENTIAL_INTEGRITY FALSE; 2 | 3 | drop table if exists proxy_mail; 4 | 5 | drop table if exists users; 6 | 7 | SET REFERENTIAL_INTEGRITY TRUE; 8 | 9 | drop sequence if exists proxy_mail_seq; 10 | 11 | -------------------------------------------------------------------------------- /forward-cat-website/indexes.sql: -------------------------------------------------------------------------------- 1 | create index ix_proxy_mail_creation_time_1 on proxy_mail (creation_time); 2 | create index ix_proxy_mail_expiration_time_1 on proxy_mail (expiration_time); 3 | create index ix_users_email_address_1 on users (email_address); -------------------------------------------------------------------------------- /forward-cat-website/app/views/user_reported_email.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, reportedUser: String, message: String) 2 | 3 | @simple_email(lang, "User reported") { 4 |

The following user has been reported: @reportedUser

5 |

Message:

6 |

@message

7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/contact_sent.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang) 2 | 3 | @simple_page(lang, Messages("contact_sent.title")(lang), Messages("contact_sent.header")(lang)) { 4 |

@Messages("contact_sent.content")(lang)

5 |

@Messages("contact_sent.thanks")(lang)

6 | } 7 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/user_reported.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang) 2 | 3 | @simple_page(lang, Messages("user_reported.title")(lang), Messages("user_reported.header")(lang)) { 4 |

@Messages("user_reported.content")(lang)

5 |

@Messages("user_reported.thanks")(lang)

6 | } 7 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/Faq.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import play.mvc.Controller; 4 | import play.mvc.Result; 5 | import play.mvc.With; 6 | import views.html.faq; 7 | 8 | @With(RedirectAction.class) 9 | public class Faq extends Controller { 10 | 11 | public Result get(String langCode) { 12 | return ok(faq.render(lang())); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-common/src/main/java/com/forwardcat/common/StringNormalizer.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.common; 2 | 3 | public class StringNormalizer { 4 | 5 | public static String onlyLowerCaseWords(String text) { 6 | return text.toLowerCase().replaceAll("[^\\w]", ""); 7 | } 8 | 9 | private StringNormalizer() { 10 | // Non-instantiable 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/email_sent.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang) 2 | 3 | @simple_page(lang, Messages("email_sent.title")(lang), Messages("email_sent.header")(lang)) { 4 |

@Messages("email_sent.content1")(lang)

5 |

@Messages("email_sent.content2")(lang)


6 |

@Messages("email_sent.back")(lang)

7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/RedirectAction.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import models.DomainManager; 4 | import play.libs.F; 5 | import play.mvc.Http; 6 | import play.mvc.Result; 7 | 8 | public class RedirectAction extends play.mvc.Action.Simple { 9 | 10 | @Override 11 | public F.Promise call(Http.Context ctx) throws Throwable { 12 | return DomainManager.redirect(ctx, delegate); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/proxy_deleted.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang) 2 | 3 | @simple_page(lang, Messages("proxy_deleted.title")(lang), Messages("proxy_deleted.header")(lang)) { 4 |

@Messages("proxy_deleted.content1")(lang)

5 |

@Messages("proxy_deleted.content2")(lang)


6 |

@Messages("proxy_deleted.back")(lang)

7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/proxy_created.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, proxy: org.apache.mailet.MailAddress, date: String) 2 | 3 | @simple_page(lang, Messages("proxy_created.title")(lang), Messages("proxy_created.header")(lang)) { 4 |

@Html(Messages("proxy_created.content1", proxy, date)(lang))

5 |

@Html(Messages("proxy_created.content2")(lang))

6 |

@Html(Messages("proxy_created.content3")(lang))

7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/Landing.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import play.mvc.Controller; 4 | import play.mvc.Result; 5 | import play.mvc.With; 6 | import views.html.forward; 7 | 8 | import javax.inject.Singleton; 9 | 10 | @Singleton 11 | @With(RedirectAction.class) 12 | public class Landing extends Controller { 13 | 14 | public Result index(String langCode) { 15 | return ok(forward.render(lang())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/proxy_extended.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, proxy: org.apache.mailet.MailAddress, date: String) 2 | 3 | @simple_page(lang, Messages("proxy_extended.title")(lang), Messages("proxy_extended.header")(lang)) { 4 |

@Html(Messages("proxy_extended.content1", proxy, date)(lang))

5 |

@Html(Messages("proxy_extended.content2")(lang))

6 |

@Html(Messages("proxy_extended.content3")(lang))

7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/confirm_deletion.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, proxy: org.apache.mailet.MailAddress, date: String, hash: String) 2 | 3 | @simple_page(lang, Messages("confirm_deletion.title")(lang), Messages("confirm_deletion.header")(lang)) { 4 |

@Html(Messages("confirm_deletion.content1", proxy, date)(lang))

5 |

@Html(Messages("confirm_deletion.content2", proxy)(lang))

6 |

@Html(Messages("confirm_deletion.deactivate", proxy, hash)(lang))


7 | } 8 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/proxy_expiring_email.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, proxy: String, date: String, hash: String) 2 | 3 | @simple_email(lang, Messages("proxy_expiring_email.title")(lang)) { 4 |

@Html(Messages("proxy_expiring_email.content1", proxy, date)(lang))

5 |

@Html(Messages("proxy_expiring_email.content2", proxy, hash)(lang))

6 |

@Html(Messages("proxy_expiring_email.content3", proxy, hash)(lang))

7 |

@Html(Messages("proxy_expiring_email.content4")(lang))

8 | } 9 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ContainsHtmlLang.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.hamcrest.core.StringContains; 5 | 6 | import static java.lang.String.format; 7 | 8 | public class ContainsHtmlLang extends StringContains { 9 | 10 | private ContainsHtmlLang(String lang) { 11 | super(format("", lang)); 12 | } 13 | 14 | public static Matcher containsHtmlLang(String lang) { 15 | return new ContainsHtmlLang(lang); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/resources/forward-cat.properties: -------------------------------------------------------------------------------- 1 | # Redis & Jedis configuration properties 2 | redis-host = localhost 3 | max-active-connections = 10 4 | min-idle-connections = 1 5 | 6 | # Ebean configuration properties 7 | datasource.pg.username=fwdcat 8 | datasource.pg.password=fwdcat-pass 9 | datasource.pg.databaseUrl=jdbc:postgresql://127.0.0.1:5432/fwdcat 10 | datasource.pg.databaseDriver=org.postgresql.Driver 11 | datasource.pg.heartbeatsql=select 1 12 | 13 | ebean.ddl.generate=false 14 | ebean.ddl.run=false -------------------------------------------------------------------------------- /forward-cat-website/app/views/error_page.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, reason: java.util.Optional[String]) 2 | 3 | @simple_page(lang, Messages("error_page.title")(lang), Messages("error_page.header")(lang)) { 4 |

@Html(Messages("error_page.content")(lang))

5 | 6 | @if(reason.isPresent) { 7 |

@Html(Messages(reason.get)(lang))

8 | } 9 | 10 |
11 |

@Messages("error_page.back")(lang)

12 | } 13 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/Stats.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.Inject; 4 | import models.StatsRepository; 5 | import play.mvc.Controller; 6 | import play.mvc.Result; 7 | import play.mvc.With; 8 | import views.html.stats; 9 | 10 | @With(RedirectAction.class) 11 | public class Stats extends Controller { 12 | 13 | private final StatsRepository statsRepo; 14 | 15 | @Inject 16 | Stats(StatsRepository statsRepo) { 17 | this.statsRepo = statsRepo; 18 | } 19 | 20 | public Result render() { 21 | return ok(stats.render(lang(), statsRepo.getStats())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /forward-cat-website/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Warn 3 | 4 | // The Typesafe repository 5 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 6 | 7 | // Use the Play sbt plugin for Play projects 8 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.10") 9 | 10 | // Web pluquins 11 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.0") 12 | 13 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7") 14 | 15 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.0.0") 16 | 17 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.1") 18 | -------------------------------------------------------------------------------- /forward-cat-website/conf/ebean.properties: -------------------------------------------------------------------------------- 1 | datasource.default=pg 2 | 3 | #datasource.h2.username=sa 4 | #datasource.h2.password= 5 | #datasource.h2.databaseUrl=jdbc:h2:mem:tests;DB_CLOSE_DELAY=-1 6 | #datasource.h2.databaseDriver=org.h2.Driver 7 | #datasource.h2.minConnections=1 8 | #datasource.h2.maxConnections=25 9 | #datasource.h2.heartbeatsql=select 1 10 | #datasource.h2.isolationlevel=read_committed 11 | 12 | datasource.pg.username=fwdcat 13 | datasource.pg.password=fwdcat-db 14 | datasource.pg.databaseUrl=jdbc:postgresql://127.0.0.1:5432/fwdcat 15 | datasource.pg.databaseDriver=org.postgresql.Driver 16 | datasource.pg.heartbeatsql=select 1 17 | 18 | ebean.ddl.generate=false 19 | ebean.ddl.run=false 20 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/proxy_created_email.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, proxy: org.apache.mailet.MailAddress, hash: String) 2 | 3 | @simple_email(lang, Messages("proxy_created_email.title")(lang)) { 4 |

@Html(Messages("proxy_created_email.intro", proxy)(lang))

5 | @defining(Messages("proxy_created_email.activation_url", proxy, hash)(lang)) { url => 6 |

@button_email(Messages("proxy_created_email.activate")(lang), url)

7 | } 8 |

@Html(Messages("proxy_created_email.deactivate", proxy, hash)(lang))

9 |

@Html(Messages("proxy_created_email.forget_it")(lang))

10 |

@Html(Messages("proxy_created_email.thanks")(lang))

11 | } 12 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/JedisHelper.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import redis.clients.jedis.Jedis; 4 | import redis.clients.jedis.JedisPool; 5 | import redis.clients.jedis.exceptions.JedisConnectionException; 6 | 7 | public class JedisHelper { 8 | 9 | public static void returnJedisOnException(JedisPool pool, Jedis jedis, Exception ex) { 10 | if (ex instanceof JedisConnectionException) { 11 | pool.returnBrokenResource(jedis); 12 | } else { 13 | pool.returnResource(jedis); 14 | } 15 | } 16 | 17 | public static void returnJedisIfNotNull(JedisPool pool, Jedis jedis) { 18 | if (jedis != null) { 19 | pool.returnResource(jedis); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/stats.scala.html: -------------------------------------------------------------------------------- 1 | @import models.StatsRepository.StatsCounters 2 | 3 | @(lang: Lang, counters: StatsCounters) 4 | 5 | @display(count: Int) = @{ 6 | "%,d" format count 7 | } 8 | 9 | @simple_page(lang, "Some Stats", "Some Stats") { 10 |

Current active users (approx): @display(counters.activeUsers)

11 |

Current active proxies (approx): @display(counters.activeProxies)

12 |

Total emails blocked: @display(counters.emailsBlocked)

13 |

Total emails forwarded: @display(counters.emailsForwarded)

14 |

Total proxies activated: @display(counters.proxiesActivated)

15 |

Total spammer proxies blocked: @display(counters.spammerProxiesBlocked)

16 | } 17 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/button_email.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String, url: String) 2 | 3 | @message 4 | -------------------------------------------------------------------------------- /forward-cat-website/conf/logger.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ${application.home}/logs/application.log 6 | 7 | %date - [%level] - from %logger in %thread %n%message%n%xException%n 8 | 9 | 10 | 11 | 12 | 13 | %date %coloredLevel %logger{15} - %message%n%xException{5} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /forward-cat-website/default-create.sql: -------------------------------------------------------------------------------- 1 | create table proxy_mail ( 2 | proxy_address varchar(255) not null, 3 | user_id varchar(40), 4 | creation_time timestamp not null, 5 | lang varchar(255) not null, 6 | expiration_time timestamp not null, 7 | blocked boolean, 8 | active boolean, 9 | expiration_notified boolean, 10 | constraint pk_proxy_mail primary key (proxy_address)) 11 | ; 12 | 13 | create table users ( 14 | id varchar(40) not null, 15 | email_address varchar(255) not null, 16 | creation_time timestamp not null, 17 | constraint pk_users primary key (id)) 18 | ; 19 | 20 | create sequence proxy_mail_seq; 21 | 22 | alter table proxy_mail add constraint fk_proxy_mail_user_1 foreign key (user_id) references users (id) on delete restrict on update restrict; 23 | create index ix_proxy_mail_user_1 on proxy_mail (user_id); 24 | 25 | 26 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/FaqTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.AbstractModule; 4 | import org.junit.Test; 5 | import play.mvc.Result; 6 | import play.test.FakeRequest; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.hamcrest.Matchers.is; 11 | import static org.junit.Assert.assertThat; 12 | import static play.test.Helpers.*; 13 | 14 | public class FaqTest extends PlayTest { 15 | @Override 16 | public AbstractModule getModule() throws IOException { 17 | return new AbstractModule() { 18 | @Override 19 | protected void configure() { 20 | // Nothing to do 21 | } 22 | }; 23 | } 24 | 25 | @Test 26 | public void everythingFine_sendFaqPage() throws Exception { 27 | Result route = route(request("/faq")); 28 | assertThat(status(route), is(OK)); 29 | } 30 | 31 | private FakeRequest request(String path) { 32 | return fakeRequest(GET, path); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/LandingTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.AbstractModule; 4 | import org.junit.Test; 5 | import play.mvc.Result; 6 | import play.test.FakeRequest; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.hamcrest.Matchers.is; 11 | import static org.junit.Assert.assertThat; 12 | import static play.test.Helpers.*; 13 | 14 | public class LandingTest extends PlayTest { 15 | @Override 16 | public AbstractModule getModule() throws IOException { 17 | return new AbstractModule() { 18 | @Override 19 | protected void configure() { 20 | // Nothing to do 21 | } 22 | }; 23 | } 24 | 25 | @Test 26 | public void everythingFine_sendLandingPage() throws Exception { 27 | Result route = route(request("/")); 28 | assertThat(status(route), is(OK)); 29 | } 30 | 31 | private FakeRequest request(String path) { 32 | return fakeRequest(GET, path); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/ValidateProxy.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.Inject; 4 | import models.Repository; 5 | import org.apache.mailet.MailAddress; 6 | import play.mvc.Controller; 7 | import play.mvc.Result; 8 | import play.mvc.With; 9 | 10 | import java.util.Optional; 11 | 12 | import static models.ControllerUtils.getMailAddress; 13 | 14 | @With(RedirectAction.class) 15 | public class ValidateProxy extends Controller { 16 | 17 | private final Repository repository; 18 | 19 | @Inject 20 | ValidateProxy(Repository repository) { 21 | this.repository = repository; 22 | } 23 | 24 | public Result validate(String proxy) { 25 | // Checking params 26 | Optional mailAddress = getMailAddress(proxy); 27 | if (!mailAddress.isPresent()) { 28 | return ok(Boolean.FALSE.toString()); 29 | } 30 | 31 | Boolean valid = !repository.proxyExists(mailAddress.get()); 32 | return ok(valid.toString()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /forward-cat-website/app/alerts/GuiceJobFactory.java: -------------------------------------------------------------------------------- 1 | package alerts; 2 | 3 | import com.google.inject.Inject; 4 | import com.google.inject.Injector; 5 | import com.google.inject.Singleton; 6 | import org.quartz.Job; 7 | import org.quartz.Scheduler; 8 | import org.quartz.SchedulerException; 9 | import org.quartz.spi.JobFactory; 10 | import org.quartz.spi.TriggerFiredBundle; 11 | 12 | /** 13 | * {@link JobFactory} that uses Guice, based on 14 | * http://www.codesmell.org/blog/2009/01/quartz-fits/ 15 | */ 16 | @Singleton 17 | public class GuiceJobFactory implements JobFactory { 18 | 19 | private final Injector injector; 20 | 21 | @Inject 22 | public GuiceJobFactory(Injector injector) { 23 | this.injector = injector; 24 | } 25 | 26 | @Override 27 | public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException { 28 | Class jobClass = bundle.getJobDetail().getJobClass(); 29 | return injector.getInstance(jobClass); 30 | } 31 | } -------------------------------------------------------------------------------- /forward-cat/forward-cat-common/src/main/java/com/forwardcat/common/RedisKeys.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.common; 2 | 3 | /** 4 | * Utility class that sets the namespaces for the Redis keys 5 | */ 6 | public class RedisKeys { 7 | 8 | public static final String PROXIES_ACTIVATED_COUNTER = generateCounterKey("proxies_activated"); 9 | public static final String EMAILS_FORWARDED_COUNTER = generateCounterKey("emails_forwarded"); 10 | public static final String EMAILS_BLOCKED_COUNTER = generateCounterKey("emails_blocked"); 11 | public static final String SPAMMER_PROXIES_BLOCKED_COUNTER = generateCounterKey("spammer_proxies_blocked"); 12 | public static final String SPAMMER_EMAILS_BLOCKED_COUNTER = generateCounterKey("spammer_emails_blocked"); 13 | 14 | public static final String USERNAME_STOPWORDS_SET = "s:username_stopwords"; 15 | 16 | /** 17 | * Given a key, returns the Redis key of the counter 18 | */ 19 | public static String generateCounterKey(String key) { 20 | return "c:" + key; 21 | } 22 | 23 | private RedisKeys() { 24 | // Non-instantiable 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/PlayTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Guice; 5 | import org.junit.After; 6 | import org.junit.Before; 7 | import play.GlobalSettings; 8 | import play.test.FakeApplication; 9 | 10 | import static play.test.Helpers.*; 11 | 12 | public abstract class PlayTest { 13 | 14 | private FakeApplication fakeApplication; 15 | 16 | @Before 17 | public void setUp() throws Exception { 18 | final AbstractModule module = getModule(); 19 | GlobalSettings testGlobal = new GlobalSettings() { 20 | @Override 21 | public A getControllerInstance(Class controllerClass) throws Exception { 22 | return Guice.createInjector(module).getInstance(controllerClass); 23 | } 24 | }; 25 | 26 | fakeApplication = fakeApplication(inMemoryDatabase(), testGlobal); 27 | start(fakeApplication); 28 | } 29 | 30 | public abstract AbstractModule getModule() throws Exception; 31 | 32 | @After 33 | public void cleanup() { 34 | stop(fakeApplication); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /forward-cat-website/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.less.Import.LessKeys 2 | import com.typesafe.sbt.web.SbtWeb 3 | import play.PlayJava 4 | import com.typesafe.sbt.web.SbtWeb.autoImport._ 5 | 6 | name := "forward-cat-web" 7 | 8 | version := "1.0-SNAPSHOT" 9 | 10 | scalaVersion := "2.11.6" 11 | 12 | lazy val root = (project in file(".")).enablePlugins(PlayJava, SbtWeb) 13 | 14 | libraryDependencies ++= Seq( 15 | javaEbean, 16 | "postgresql" % "postgresql" % "9.1-901-1.jdbc4", 17 | "redis.clients" % "jedis" % "2.1.0", 18 | "org.apache.james" % "apache-mailet" % "2.4", 19 | "com.google.inject" % "guice" % "3.0", 20 | "javax.mail" % "mail" % "1.4.1", 21 | "org.quartz-scheduler" % "quartz" % "2.1.7", 22 | "com.forwardcat" % "common" % "1.0-SNAPSHOT", 23 | "org.hamcrest" % "hamcrest-all" % "1.3" % "test", 24 | "org.mockito" % "mockito-all" % "1.9.5" % "test" 25 | ) 26 | 27 | resolvers += ("Local Maven Repository" at "file:///"+Path.userHome.absolutePath+"/.m2/repository") 28 | 29 | pipelineStages := Seq(rjs) 30 | 31 | includeFilter in (Assets, LessKeys.less) := "*.less" 32 | 33 | excludeFilter in (Assets, LessKeys.less) := "_*.less" 34 | 35 | LessKeys.compress := true -------------------------------------------------------------------------------- /forward-cat/README.md: -------------------------------------------------------------------------------- 1 | # Forward Cat 2 | Forward Cat is a service that allows you to quickly create a temporary email address that automatically forwards to your real email address. 3 | 4 | ## How to build it 5 | In order to build Forward Cat you need: 6 | 7 | * Java 7+ 8 | * Maven 3+ 9 | 10 | Just head into the root directory and type: 11 | 12 | mvn clean install 13 | 14 | 15 | ## Structure 16 | The project is divided in three modules: the email server, the web server and some common classes shared between the other two. 17 | 18 | ### Web Server [forward-cat-server] 19 | To serve the website we use a custom framework built on [Netty](http://netty.io/). It manages the creation of temporary email addresses, which are stored in a [Redis](http://redis.io/) database. 20 | 21 | ### Email Server [forward-cat-james] 22 | To send and receive emails, we use [Apache James](http://james.apache.org/). When receiving a new email, we check whether a valid temporary email address exists and we forward the email accordingly. 23 | 24 | ### Shared Classes [forward-cat-common] 25 | Since the temporary email addresses are stored as JSON values, this module contains the class that maps them. To do so, we us [Jackson](http://jackson.codehaus.org/). 26 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/Contact.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.Inject; 4 | import models.MailSender; 5 | import org.apache.mailet.MailAddress; 6 | import play.Play; 7 | import play.mvc.Controller; 8 | import play.mvc.Result; 9 | import play.mvc.With; 10 | import play.twirl.api.Html; 11 | import views.html.contact; 12 | import views.html.contact_email; 13 | import views.html.contact_sent; 14 | 15 | import javax.mail.internet.AddressException; 16 | 17 | @With(RedirectAction.class) 18 | public class Contact extends Controller { 19 | 20 | private final MailSender mailSender; 21 | private final MailAddress contactAddress; 22 | 23 | @Inject 24 | Contact(MailSender mailSender) throws AddressException { 25 | this.mailSender = mailSender; 26 | this.contactAddress = new MailAddress(Play.application().configuration().getString("contactAddress")); 27 | } 28 | 29 | public Result contactGet(String langCode) { 30 | return ok(contact.render(lang())); 31 | } 32 | 33 | public Result contactSent(String langCode, String email, String message) { 34 | Html content = contact_email.render(lang(), email, message); 35 | mailSender.sendHtmlMail(contactAddress, "Contact", content.toString()); 36 | return ok(contact_sent.render(lang())); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/contact.scala.html: -------------------------------------------------------------------------------- 1 | @import models.DomainManager.buildUrl 2 | @(lang: Lang) 3 | 4 | @simple_page(lang, Messages("contact.title")(lang), Messages("contact.header")(lang)) { 5 |

@Messages("contact.content")(lang)


6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 |

14 |
15 |
16 |
17 | 18 | 19 |
20 | 21 |

22 |
23 |
24 |
25 | 26 |
27 |
28 | } 29 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/report.scala.html: -------------------------------------------------------------------------------- 1 | @import models.DomainManager.buildUrl 2 | @(lang: Lang) 3 | 4 | @simple_page(lang, Messages("report_user.title")(lang), Messages("report_user.header")(lang)) { 5 |

@Messages("report_proxy.content")(lang)


6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 |

14 |
15 |
16 |
17 | 18 | 19 |
20 | 21 |

22 |
23 |
24 |
25 | 26 |
27 |
28 | } 29 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/MailAddressNormalizer.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.google.common.base.Throwables; 4 | import org.apache.mailet.MailAddress; 5 | 6 | import javax.mail.internet.AddressException; 7 | import java.util.Optional; 8 | 9 | public class MailAddressNormalizer { 10 | 11 | public static Optional normalize(Optional address) { 12 | return address.map(MailAddressNormalizer::normalize); 13 | } 14 | 15 | public static MailAddress normalize(MailAddress mailAddress) { 16 | if (isGmailAddress(mailAddress)) { 17 | String normalizedLocalPart = mailAddress.getLocalPart() 18 | .replace(".", ""); 19 | 20 | int plusSign = normalizedLocalPart.indexOf("+"); 21 | if (plusSign >= 0) { 22 | normalizedLocalPart = normalizedLocalPart.substring(0, plusSign); 23 | } 24 | 25 | try { 26 | return new MailAddress(normalizedLocalPart, mailAddress.getDomain()); 27 | } catch (AddressException e) { 28 | Throwables.propagate(e); 29 | } 30 | } 31 | return mailAddress; 32 | } 33 | 34 | private static boolean isGmailAddress(MailAddress address) { 35 | return "gmail.com".equals(address.getDomain()); 36 | } 37 | 38 | private MailAddressNormalizer() { 39 | // Non-instantiable 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/resources/lmtpserver.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/resources/imapserver.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/resources/pop3server.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /forward-cat-website/test/models/MailAddressNormalizerTest.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.google.common.base.Throwables; 4 | import com.google.common.collect.ImmutableMap; 5 | import org.apache.mailet.MailAddress; 6 | import org.junit.Test; 7 | 8 | import javax.mail.internet.AddressException; 9 | import java.util.Map; 10 | 11 | import static models.MailAddressNormalizer.normalize; 12 | import static org.hamcrest.core.Is.is; 13 | import static org.junit.Assert.assertThat; 14 | 15 | public class MailAddressNormalizerTest { 16 | 17 | Map addressessPairs = ImmutableMap.builder() 18 | .put("test@mail.com", "test@mail.com") 19 | .put("dots.test@gmail.com", "dotstest@gmail.com") 20 | .put("mail+test@gmail.com", "mail@gmail.com") 21 | .put("mail.dots+test@gmail.com", "maildots@gmail.com") 22 | .build(); 23 | 24 | @Test 25 | public void testNormalize() { 26 | for (Map.Entry pair : addressessPairs.entrySet()) { 27 | MailAddress original = toMail(pair.getKey()); 28 | MailAddress expected = toMail(pair.getValue()); 29 | MailAddress normalized = normalize(original); 30 | 31 | assertThat(normalized.toString(), is(expected.toString())); 32 | } 33 | } 34 | 35 | private MailAddress toMail(String address) { 36 | try { 37 | return new MailAddress(address); 38 | } catch (AddressException e) { 39 | throw Throwables.propagate(e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/navbar.scala.html: -------------------------------------------------------------------------------- 1 | @import models.DomainManager.buildUrl 2 | @(lang: Lang) 3 | 4 |
30 | -------------------------------------------------------------------------------- /forward-cat-website/test/models/DomainManagerTest.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import play.GlobalSettings; 7 | import play.i18n.Lang; 8 | import play.test.FakeApplication; 9 | 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.junit.Assert.assertThat; 12 | import static play.test.Helpers.*; 13 | 14 | public class DomainManagerTest { 15 | 16 | Lang EN = new Lang(Lang.get("en").get()); 17 | Lang IT = new Lang(Lang.get("it").get()); 18 | Lang ES = new Lang(Lang.get("es").get()); 19 | 20 | FakeApplication fakeApplication; 21 | 22 | @Before 23 | public void setup() { 24 | fakeApplication = fakeApplication(inMemoryDatabase(), new GlobalSettings()); 25 | start(fakeApplication); 26 | } 27 | 28 | @After 29 | public void cleanup() { 30 | stop(fakeApplication); 31 | } 32 | 33 | @Test 34 | public void buildUrl_defaultLanguage_doesntSetLanguageInPath() throws Exception { 35 | String path = DomainManager.buildUrl(EN, "/my-path"); 36 | assertThat(path, is("/my-path")); 37 | } 38 | 39 | @Test(expected = IllegalArgumentException.class) 40 | public void buildUrl_unknownLanguage_throwsException() throws Exception { 41 | DomainManager.buildUrl(IT, "/my-path"); 42 | } 43 | 44 | @Test 45 | public void buildUrl_notDefaultLanguage_languageIsSetInPath() throws Exception { 46 | String path = DomainManager.buildUrl(ES, "/my-path"); 47 | assertThat(path, is("/es/my-path")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/java/com/forwardcat/james/MailUtils.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.james; 2 | 3 | import com.forwardcat.common.StringNormalizer; 4 | import com.google.common.collect.Lists; 5 | import org.apache.mailet.Mail; 6 | import org.apache.mailet.MailAddress; 7 | 8 | import java.util.List; 9 | 10 | public class MailUtils { 11 | 12 | private static final List USER_IGNORE_KEYWORDS = Lists.newArrayList("noreply", "notreply", "notrespond", "no-responder"); 13 | private static final List FB_USER_IGNORE_KEYWORDS = Lists.newArrayList("notification", "update"); 14 | 15 | public static boolean shouldBounce(Mail mail) { 16 | MailAddress sender = mail.getSender(); 17 | 18 | boolean notRespondSender = false; 19 | boolean fbNotification = false; 20 | if (sender != null && sender.getLocalPart() != null) { 21 | String senderUser = StringNormalizer.onlyLowerCaseWords(sender.getLocalPart()); 22 | notRespondSender = containsAny(senderUser, USER_IGNORE_KEYWORDS); 23 | 24 | if (sender.getDomain() != null) { 25 | String senderDomain = sender.getDomain().toLowerCase(); 26 | fbNotification = "facebookmail.com".equals(senderDomain) && containsAny(senderUser, FB_USER_IGNORE_KEYWORDS); 27 | } 28 | } 29 | 30 | boolean toIgnore = notRespondSender || fbNotification; 31 | return !toIgnore; 32 | } 33 | 34 | private static boolean containsAny(String string, List contained) { 35 | return contained.stream().anyMatch(string::contains); 36 | } 37 | 38 | private MailUtils() { 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/SpamCatcher.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.forwardcat.common.RedisKeys; 4 | import com.google.common.base.Throwables; 5 | import com.google.inject.Inject; 6 | import org.apache.mailet.MailAddress; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import redis.clients.jedis.Jedis; 10 | import redis.clients.jedis.JedisPool; 11 | 12 | import java.util.Set; 13 | 14 | import static models.JedisHelper.returnJedisIfNotNull; 15 | import static models.JedisHelper.returnJedisOnException; 16 | 17 | public class SpamCatcher { 18 | 19 | private static Logger LOGGER = LoggerFactory.getLogger(SpamCatcher.class.getName()); 20 | private final Set usernameStopwords; 21 | 22 | @Inject 23 | public SpamCatcher(JedisPool jedisPool) { 24 | usernameStopwords = getUsernameStopwords(jedisPool); 25 | LOGGER.info("Username stopwords loaded: " + usernameStopwords.toString()); 26 | } 27 | 28 | public boolean isSpam(MailAddress proxyAddress) { 29 | String user = normalize(proxyAddress.getLocalPart()); 30 | return usernameStopwords.stream().anyMatch(user::contains); 31 | } 32 | 33 | private String normalize(String text) { 34 | return text.toLowerCase() 35 | .replaceAll("[^\\w]", ""); 36 | } 37 | 38 | private Set getUsernameStopwords(JedisPool jedisPool) { 39 | Jedis jedis = null; 40 | try { 41 | jedis = jedisPool.getResource(); 42 | return jedis.smembers(RedisKeys.USERNAME_STOPWORDS_SET); 43 | } catch (Exception ex) { 44 | returnJedisOnException(jedisPool, jedis, ex); 45 | jedis = null; 46 | throw Throwables.propagate(ex); 47 | } finally { 48 | returnJedisIfNotNull(jedisPool, jedis); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /forward-cat-website/test/models/ControllerUtilsTest.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import org.apache.mailet.MailAddress; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import play.GlobalSettings; 8 | import play.i18n.Lang; 9 | import play.mvc.Http; 10 | import play.test.FakeApplication; 11 | 12 | import java.util.Optional; 13 | 14 | import static models.ControllerUtils.getBestLanguage; 15 | import static models.ControllerUtils.toMailAddress; 16 | import static org.hamcrest.CoreMatchers.is; 17 | import static org.hamcrest.Matchers.not; 18 | import static org.junit.Assert.assertThat; 19 | import static play.test.Helpers.*; 20 | 21 | public class ControllerUtilsTest { 22 | 23 | Lang EN = new Lang(Lang.get("en").get()); 24 | FakeApplication fakeApplication; 25 | 26 | @Before 27 | public void setup() { 28 | fakeApplication = fakeApplication(inMemoryDatabase(), new GlobalSettings()); 29 | start(fakeApplication); 30 | } 31 | 32 | @After 33 | public void cleanup() { 34 | stop(fakeApplication); 35 | } 36 | 37 | @Test 38 | public void getParamValueAsMailAddress_invalidMail_returnsNull() throws Exception { 39 | Optional email = toMailAddress("not a mail"); 40 | assertThat(email, is(Optional.empty())); 41 | } 42 | 43 | @Test 44 | public void getParamValueAsMailAddress_correctParam_returnsMailAddress() throws Exception { 45 | Optional email = toMailAddress("my@mail.com"); 46 | assertThat(email, is(not(Optional.empty()))); 47 | } 48 | 49 | @Test 50 | public void unknownHost_shouldReturnDefaultLang() { 51 | Http.Request request = new TestHttpRequest().setHost("zz.forward.cat"); 52 | Lang language = getBestLanguage(request, EN); 53 | assertThat(language.code(), is("en")); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /forward-cat-website/test/models/SpamCatcherTest.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.forwardcat.common.RedisKeys; 4 | import com.google.common.base.Throwables; 5 | import com.google.common.collect.Sets; 6 | import org.apache.mailet.MailAddress; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | import redis.clients.jedis.Jedis; 13 | import redis.clients.jedis.JedisPool; 14 | 15 | import javax.mail.internet.AddressException; 16 | import java.util.Set; 17 | 18 | import static org.junit.Assert.assertFalse; 19 | import static org.junit.Assert.assertTrue; 20 | import static org.mockito.Mockito.when; 21 | 22 | @RunWith(MockitoJUnitRunner.class) 23 | public class SpamCatcherTest { 24 | 25 | SpamCatcher spamCatcher; 26 | Set usernameStopwords = Sets.newHashSet("spammer", "buynow"); 27 | 28 | @Mock JedisPool jedisPool; 29 | @Mock Jedis jedis; 30 | 31 | @Before 32 | public void setup() { 33 | when(jedisPool.getResource()).thenReturn(jedis); 34 | when(jedis.smembers(RedisKeys.USERNAME_STOPWORDS_SET)).thenReturn(usernameStopwords); 35 | spamCatcher = new SpamCatcher(jedisPool); 36 | } 37 | 38 | @Test 39 | public void normalAddressIsNotSpam() { 40 | MailAddress address = address("jim@forward.cat"); 41 | assertFalse(spamCatcher.isSpam(address)); 42 | } 43 | 44 | @Test 45 | public void addressWithKeywordIsSpam() { 46 | MailAddress address = address("hurry-Buy.Now@forward.cat"); 47 | assertTrue(spamCatcher.isSpam(address)); 48 | } 49 | 50 | private MailAddress address(String username) { 51 | try { 52 | return new MailAddress(username); 53 | } catch (AddressException e) { 54 | throw Throwables.propagate(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-common/src/main/java/com/forwardcat/common/User.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.common; 2 | 3 | import com.google.common.collect.Sets; 4 | import org.apache.mailet.MailAddress; 5 | 6 | import javax.persistence.*; 7 | import java.util.Date; 8 | import java.util.Set; 9 | import java.util.UUID; 10 | 11 | @Entity(name = "users") 12 | @Table(name = "users") 13 | public class User { 14 | 15 | @Id @Column(nullable = false) 16 | private UUID id; 17 | @Column(nullable = false) 18 | private String emailAddress; 19 | @Column(nullable = false) @Temporal(TemporalType.TIMESTAMP) 20 | private Date creationTime; 21 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "user") 22 | private Set proxies; 23 | 24 | public UUID getId() { 25 | return id; 26 | } 27 | 28 | public void setId(UUID id) { 29 | this.id = id; 30 | } 31 | 32 | public String getEmailAddress() { 33 | return emailAddress; 34 | } 35 | 36 | public void setEmailAddress(String emailAddress) { 37 | this.emailAddress = emailAddress; 38 | } 39 | 40 | public Date getCreationTime() { 41 | return creationTime; 42 | } 43 | 44 | public void setCreationTime(Date creationTime) { 45 | this.creationTime = creationTime; 46 | } 47 | 48 | public Set getProxies() { 49 | return proxies; 50 | } 51 | 52 | public void setProxies(Set proxies) { 53 | this.proxies = proxies; 54 | } 55 | 56 | public static User create(MailAddress userAddress, Date creationTime, ProxyMail ... proxies) { 57 | User user = new User(); 58 | user.setId(UUID.randomUUID()); 59 | user.setEmailAddress(userAddress.toString().toLowerCase()); 60 | user.setCreationTime(creationTime); 61 | user.setProxies(Sets.newHashSet(proxies)); 62 | return user; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ContactTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.AbstractModule; 4 | import models.MailSender; 5 | import org.apache.mailet.MailAddress; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | import play.mvc.Result; 11 | import play.test.FakeRequest; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.hamcrest.Matchers.is; 16 | import static org.junit.Assert.assertThat; 17 | import static org.mockito.Matchers.any; 18 | import static org.mockito.Matchers.anyString; 19 | import static org.mockito.Mockito.verify; 20 | import static play.test.Helpers.*; 21 | 22 | @RunWith(MockitoJUnitRunner.class) 23 | public class ContactTest extends PlayTest { 24 | 25 | @Mock MailSender mailSender; 26 | 27 | @Override 28 | public AbstractModule getModule() throws IOException { 29 | return new AbstractModule() { 30 | @Override 31 | protected void configure() { 32 | bind(MailSender.class).toInstance(mailSender); 33 | } 34 | }; 35 | } 36 | 37 | @Test 38 | public void contactPageRequested_isServed() throws Exception { 39 | Result route = route(contactLandingRequest()); 40 | assertThat(status(route), is(OK)); 41 | } 42 | 43 | @Test 44 | public void contactSentRequested_emailSentAndPageServed() throws Exception { 45 | Result route = route(contactRequest()); 46 | verify(mailSender).sendHtmlMail(any(MailAddress.class), anyString(), anyString()); 47 | assertThat(status(route), is(OK)); 48 | } 49 | 50 | private FakeRequest contactLandingRequest() { 51 | return fakeRequest(GET, "/contact"); 52 | } 53 | 54 | private FakeRequest contactRequest() { 55 | return fakeRequest(GET, "/contact-sent?email=me@test.com&message=Nice website!"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/MailSender.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import org.apache.mailet.MailAddress; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import play.Play; 7 | 8 | import javax.annotation.concurrent.ThreadSafe; 9 | import javax.mail.Message; 10 | import javax.mail.Session; 11 | import javax.mail.Transport; 12 | import javax.mail.internet.AddressException; 13 | import javax.mail.internet.InternetAddress; 14 | import javax.mail.internet.MimeMessage; 15 | import java.util.Properties; 16 | 17 | @ThreadSafe 18 | public class MailSender { 19 | 20 | private static final String TEXT_HTML = "text/html; charset=UTF-8"; 21 | 22 | private final Properties mailServerProperties = System.getProperties(); 23 | private final InternetAddress senderAddress; 24 | 25 | public MailSender() throws AddressException { 26 | String mailServerHost = Play.application().configuration().getString("mailServerHost"); 27 | mailServerProperties.setProperty("mail.smtp.host", mailServerHost); 28 | 29 | String senderAddressStr = Play.application().configuration().getString("mailSenderAddress"); 30 | this.senderAddress = new InternetAddress(senderAddressStr); 31 | } 32 | 33 | public void sendHtmlMail(MailAddress to, String subject, String content) { 34 | Session session = Session.getDefaultInstance(mailServerProperties); 35 | 36 | try { 37 | MimeMessage message = new MimeMessage(session); 38 | message.setFrom(senderAddress); 39 | message.addRecipient(Message.RecipientType.TO, 40 | new InternetAddress(to.toString())); 41 | message.setSubject(subject); 42 | message.setContent(content, TEXT_HTML); 43 | 44 | Transport.send(message); 45 | } catch (Exception ex) { 46 | LOGGER.error("Unexpected error", ex); 47 | } 48 | } 49 | 50 | private static Logger LOGGER = LoggerFactory.getLogger(MailSender.class.getName()); 51 | } 52 | -------------------------------------------------------------------------------- /forward-cat-website/app/Global.java: -------------------------------------------------------------------------------- 1 | import alerts.SendAlertJob; 2 | import com.google.common.base.Throwables; 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import models.Options; 6 | import org.quartz.*; 7 | import play.Application; 8 | import play.GlobalSettings; 9 | 10 | public class Global extends GlobalSettings { 11 | 12 | private static final String JOB_NAME = "sendAlertJob"; 13 | private static final Injector INJECTOR = Guice.createInjector(new Module()); 14 | private Scheduler alertScheduler; 15 | private Options options; 16 | 17 | @Override 18 | public void onStart(Application application) { 19 | alertScheduler = INJECTOR.getInstance(Scheduler.class); 20 | options = INJECTOR.getInstance(Options.class); 21 | 22 | scheduleAlerts(); 23 | } 24 | 25 | @Override 26 | public void onStop(Application application) { 27 | try { 28 | alertScheduler.shutdown(); 29 | } catch (SchedulerException e) { 30 | Throwables.propagate(e); 31 | } 32 | } 33 | 34 | @Override 35 | public A getControllerInstance(Class controllerClass) throws Exception { 36 | return INJECTOR.getInstance(controllerClass); 37 | } 38 | 39 | private void scheduleAlerts() { 40 | JobDetail jobDetail = JobBuilder.newJob(SendAlertJob.class) 41 | .withIdentity(new JobKey(JOB_NAME)) 42 | .build(); 43 | 44 | SimpleScheduleBuilder schedule = SimpleScheduleBuilder 45 | .repeatMinutelyForever(options.getMinutesBetweenAlerts()); 46 | 47 | Trigger trigger = TriggerBuilder.newTrigger() 48 | .withIdentity(JOB_NAME) 49 | .startNow() 50 | .withSchedule(schedule) 51 | .build(); 52 | 53 | try { 54 | alertScheduler.scheduleJob(jobDetail, trigger); 55 | } catch (SchedulerException e) { 56 | throw Throwables.propagate(e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /forward-cat-website/test/alerts/SendAlertJobTest.java: -------------------------------------------------------------------------------- 1 | package alerts; 2 | 3 | 4 | import com.forwardcat.common.ProxyMail; 5 | import com.google.common.collect.Sets; 6 | import models.MailSender; 7 | import models.Options; 8 | import models.Repository; 9 | import org.apache.mailet.MailAddress; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | import org.quartz.JobExecutionContext; 16 | 17 | import java.io.IOException; 18 | 19 | import static controllers.TestUtils.activeProxy; 20 | import static controllers.TestUtils.toMailAddress; 21 | import static org.mockito.Matchers.any; 22 | import static org.mockito.Matchers.anyString; 23 | import static org.mockito.Mockito.*; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class SendAlertJobTest { 27 | 28 | SendAlertJob job; 29 | 30 | MailAddress proxyMail = toMailAddress("test@forward.cat"); 31 | ProxyMail proxy = activeProxy(proxyMail, false); 32 | 33 | @Mock MailSender mailSender; 34 | @Mock JobExecutionContext ctx; 35 | @Mock Options opts; 36 | @Mock Repository repository; 37 | 38 | @Before 39 | public void setUp() throws IOException { 40 | when(opts.getTimeBetweenAlertMailsMillis()).thenReturn(1); 41 | 42 | job = new SendAlertJob(repository, mailSender, opts); 43 | } 44 | 45 | @Test 46 | public void noAlerts_doNothing() throws Exception { 47 | job.execute(ctx); 48 | 49 | verify(mailSender, never()).sendHtmlMail(any(MailAddress.class), anyString(), anyString()); 50 | verify(repository, never()).update(any(ProxyMail.class)); 51 | } 52 | 53 | @Test 54 | public void someAlerts_sendMailsAndRemoveThem() throws Exception { 55 | when(repository.getExpiringProxies()).thenReturn(Sets.newHashSet(proxy)); 56 | job.execute(ctx); 57 | 58 | verify(mailSender, times(1)).sendHtmlMail(any(MailAddress.class), anyString(), anyString()); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/Report.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.Inject; 5 | import models.MailSender; 6 | import models.Repository; 7 | import org.apache.mailet.MailAddress; 8 | import play.Play; 9 | import play.mvc.Controller; 10 | import play.mvc.Result; 11 | import play.mvc.With; 12 | import play.twirl.api.Html; 13 | import views.html.*; 14 | 15 | import javax.mail.internet.AddressException; 16 | import java.util.Optional; 17 | 18 | import static models.ControllerUtils.isLocal; 19 | import static models.ControllerUtils.toMailAddress; 20 | 21 | @With(RedirectAction.class) 22 | public class Report extends Controller { 23 | 24 | private final Repository repository; 25 | private final MailSender mailSender; 26 | private final MailAddress contactAddress; 27 | 28 | @Inject 29 | Report(Repository repository, MailSender mailSender) throws AddressException { 30 | this.repository = repository; 31 | this.mailSender = mailSender; 32 | this.contactAddress = new MailAddress(Play.application().configuration().getString("contactAddress")); 33 | } 34 | 35 | public Result reportGet(String langCode) { 36 | return ok(report.render(lang())); 37 | } 38 | 39 | public Result reportUser(String langCode, String proxy, String message) { 40 | Optional mailAddress = toMailAddress(proxy); 41 | if (mailAddress.isPresent() && isLocal(mailAddress.get())) { 42 | 43 | Optional maybeProxyMail = repository.getProxy(mailAddress.get()); 44 | if (maybeProxyMail.isPresent()) { 45 | 46 | ProxyMail proxyMail = maybeProxyMail.get(); 47 | if (proxyMail.isActive() && !proxyMail.isBlocked()) { 48 | Html content = user_reported_email.render(lang(), proxy, message); 49 | mailSender.sendHtmlMail(contactAddress, "User reported", content.toString()); 50 | } 51 | } 52 | } 53 | 54 | return ok(user_reported.render(lang())); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/ExpirationUtils.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import play.i18n.Lang; 4 | 5 | import java.time.ZoneOffset; 6 | import java.time.ZonedDateTime; 7 | import java.time.format.DecimalStyle; 8 | import java.util.Date; 9 | import java.util.Locale; 10 | 11 | import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; 12 | 13 | /** 14 | * Collection of methods for dealing with time and expiration dates 15 | */ 16 | public class ExpirationUtils { 17 | 18 | // Durations 19 | private static final int UNCONFIRMED_PROXY_EXPIRATION_SEC = 60 * 30; 20 | private static final int MAX_PROXY_DURATION_DAYS = 15; 21 | private static final int INCREMENT_DAYS_ADDED = 5; 22 | 23 | /** 24 | * Formats a {@link Date} with the given {@link Locale} in a user 25 | * friendly way 26 | */ 27 | public static String formatInstant(Date date, Lang language) { 28 | Locale locale = language.toLocale(); 29 | return ISO_OFFSET_DATE_TIME.withDecimalStyle(DecimalStyle.of(locale)).format(toZonedDateTime(date)); 30 | } 31 | 32 | public static Date toDate(ZonedDateTime dateTime) { 33 | return Date.from(dateTime.toInstant()); 34 | } 35 | 36 | public static ZonedDateTime toZonedDateTime(Date date) { 37 | return ZonedDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC); 38 | } 39 | 40 | public static ZonedDateTime now() { 41 | return ZonedDateTime.now(ZoneOffset.UTC); 42 | } 43 | 44 | /** 45 | * Returns the number of days that a proxy can be active 46 | */ 47 | public static int getMaxProxyDuration() { 48 | return MAX_PROXY_DURATION_DAYS; 49 | } 50 | 51 | /** 52 | * Returns the number of days that a proxy can be extended 53 | */ 54 | public static int getIncrementDaysAdded() { 55 | return INCREMENT_DAYS_ADDED; 56 | } 57 | 58 | /** 59 | * Returns the number of seconds that an unconfirmed proxy will last 60 | */ 61 | public static int getUnconfirmedProxyDuration() { 62 | return UNCONFIRMED_PROXY_EXPIRATION_SEC; 63 | } 64 | 65 | private ExpirationUtils() { 66 | // Non-instantiable 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /forward-cat-website/app/Module.java: -------------------------------------------------------------------------------- 1 | import alerts.GuiceJobFactory; 2 | import com.google.common.base.Throwables; 3 | import com.google.inject.AbstractModule; 4 | import com.google.inject.Provides; 5 | import com.google.inject.Singleton; 6 | import models.MailSender; 7 | import models.Options; 8 | import models.SpamCatcher; 9 | import org.quartz.Scheduler; 10 | import org.quartz.SchedulerException; 11 | import org.quartz.impl.StdSchedulerFactory; 12 | import redis.clients.jedis.JedisPool; 13 | import redis.clients.jedis.JedisPoolConfig; 14 | 15 | public class Module extends AbstractModule { 16 | 17 | private final Options options = new Options(); 18 | 19 | @Override 20 | protected void configure() { 21 | // Performing bindings 22 | bind(Options.class).toInstance(options); 23 | bind(MailSender.class).in(Singleton.class); 24 | bind(SpamCatcher.class).in(Singleton.class); 25 | } 26 | 27 | @Provides 28 | @Singleton 29 | public JedisPool providesJedisPool() { 30 | // Configuring the connection pool 31 | JedisPoolConfig poolConfig = new JedisPoolConfig(); 32 | poolConfig.setMaxActive(options.getMaxActiveConnections()); 33 | poolConfig.setTestOnBorrow(options.isTestConnectionOnBorrow()); 34 | poolConfig.setTestOnReturn(options.isTestConnectionOnReturn()); 35 | poolConfig.setMaxIdle(options.getMaxIdleConnections()); 36 | poolConfig.setMinIdle(options.getMinIdleConnections()); 37 | poolConfig.setTestWhileIdle(options.isTestConnectionWhileIdle()); 38 | poolConfig.setNumTestsPerEvictionRun(options.getNumTestsPerEvictionRun()); 39 | poolConfig.setTimeBetweenEvictionRunsMillis(options.getTimeBetweenEvictionRunsMillis()); 40 | 41 | return new JedisPool(poolConfig, options.getRedisHost(), options.getRedisPort()); 42 | } 43 | 44 | @Provides 45 | public Scheduler providesScheduler(GuiceJobFactory factory, StdSchedulerFactory schedulerFactory) throws SchedulerException { 46 | try { 47 | schedulerFactory.initialize("quartz-scheduler.properties"); 48 | Scheduler scheduler = schedulerFactory.getScheduler(); 49 | scheduler.setJobFactory(factory); 50 | scheduler.start(); 51 | return scheduler; 52 | } catch (SchedulerException e) { 53 | throw Throwables.propagate(e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /forward-cat/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.forwardcat 8 | parent 9 | 1.0-SNAPSHOT 10 | pom 11 | Forward Cat 12 | 13 | 14 | forward-cat-james 15 | forward-cat-common 16 | 17 | 18 | 19 | 20 | org.avaje.ebeanorm 21 | avaje-ebeanorm-api 22 | 3.1.1 23 | provided 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 3.0 34 | 35 | 1.8 36 | 1.8 37 | 38 | 39 | 40 | 41 | 42 | org.avaje.ebeanorm 43 | avaje-ebeanorm-mavenenhancer 44 | 3.3.2 45 | 46 | com.forwardcat.common.** 47 | debug=1 48 | 49 | 50 | 51 | 52 | main 53 | process-classes 54 | 55 | enhance 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /forward-cat-website/app/assets/stylesheets/styles.less: -------------------------------------------------------------------------------- 1 | .cat-img { 2 | margin: auto; 3 | } 4 | 5 | @media (min-width:400px) { 6 | .cat-img { 7 | background-image: url('/assets/images/forward.png'); width: 321px; height: 215px; 8 | visibility: visible; 9 | } 10 | } 11 | @media (max-width:400px) { 12 | .cat-img { 13 | background-image: none; 14 | visibility: hidden; 15 | } 16 | } 17 | 18 | .error { 19 | color: #cc0000; 20 | } 21 | 22 | .info-icon { 23 | color: #3A87AD; 24 | } 25 | 26 | .faq h4 { 27 | margin-top: 2em 28 | } 29 | 30 | // From: http://getbootstrap.com/examples/jumbotron-narrow/jumbotron-narrow.css 31 | 32 | /* Space out content a bit */ 33 | body { 34 | padding-top: 20px; 35 | padding-bottom: 20px; 36 | } 37 | 38 | /* Everything but the jumbotron gets side spacing for mobile first views */ 39 | .header, 40 | .marketing, 41 | .footer { 42 | padding-right: 15px; 43 | padding-left: 15px; 44 | } 45 | 46 | /* Custom page header */ 47 | .header { 48 | padding-bottom: 20px; 49 | border-bottom: 1px solid #e5e5e5; 50 | } 51 | 52 | /* Custom page footer */ 53 | .footer { 54 | color: #777; 55 | } 56 | 57 | /* Customize container */ 58 | @media (min-width: 768px) { 59 | .container { 60 | max-width: 730px; 61 | } 62 | } 63 | 64 | .section { 65 | margin-bottom: 15px; 66 | } 67 | 68 | .navbar-default { 69 | border-bottom: solid 2px #815A5A !important; 70 | } 71 | 72 | .navbar-default .navbar-toggle .icon-bar { 73 | background-color: #815A5A !important; 74 | } 75 | 76 | /* Main marketing message and sign up button */ 77 | .jumbotron { 78 | margin: 0; 79 | padding: 40px 0 0 0 !important; 80 | background-color: transparent !important; 81 | text-align: center; 82 | } 83 | .jumbotron h1 { 84 | font-size: 45px; 85 | line-height: 1; 86 | } 87 | 88 | .jumbotron .btn { 89 | padding: 14px 24px; 90 | font-size: 21px; 91 | } 92 | 93 | /* Supporting marketing content */ 94 | .marketing { 95 | margin: 40px 0; 96 | } 97 | .marketing p + h4 { 98 | margin-top: 28px; 99 | } 100 | 101 | /* Responsive: Portrait tablets and up */ 102 | @media screen and (min-width: 768px) { 103 | /* Remove the padding we set earlier */ 104 | .header, 105 | .marketing, 106 | .footer { 107 | padding-right: 0; 108 | padding-left: 0; 109 | } 110 | /* Space out the masthead */ 111 | .header { 112 | margin-bottom: 30px; 113 | } 114 | /* Remove the bottom border on the jumbotron for visual effect */ 115 | .jumbotron { 116 | border-bottom: 0; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/simple_page.scala.html: -------------------------------------------------------------------------------- 1 | @import models.DomainManager.buildUrl 2 | @(lang: Lang, title: String, header: String)(content: Html) 3 | 4 | 5 | 6 | 7 | 8 | @title - Forward Cat 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 32 | 33 | 34 |
35 | @navbar(lang) 36 | 37 |
38 | 39 | 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/TestUtils.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.forwardcat.common.User; 5 | import com.google.common.base.Throwables; 6 | import models.Repository; 7 | import org.apache.mailet.MailAddress; 8 | 9 | import javax.mail.internet.AddressException; 10 | import java.util.Date; 11 | import java.util.Optional; 12 | 13 | import static java.lang.String.format; 14 | import static org.mockito.Matchers.any; 15 | import static org.mockito.Mockito.when; 16 | 17 | public class TestUtils { 18 | 19 | public static ProxyMail activeProxy(MailAddress address, boolean blocked) { 20 | return proxy(address, blocked, true); 21 | } 22 | 23 | public static ProxyMail inactiveProxy(MailAddress address) { 24 | return proxy(address, false, false); 25 | } 26 | 27 | private static ProxyMail proxy(MailAddress address, boolean blocked, boolean active) { 28 | ProxyMail proxyMail = ProxyMail.create(address, toMailAddress("user@mail.com"), new Date(), new Date(), "en"); 29 | if (blocked) { 30 | proxyMail.block(); 31 | } 32 | if (active) { 33 | proxyMail.activate(); 34 | } 35 | proxyMail.setUser(User.create(toMailAddress("user@address.com"), new Date())); 36 | return proxyMail; 37 | } 38 | 39 | public static MailAddress toMailAddress(String mailAddress) { 40 | try { 41 | return new MailAddress(mailAddress); 42 | } catch (AddressException e) { 43 | throw Throwables.propagate(e); 44 | } 45 | } 46 | 47 | public static void whenAddressReturnProxy(Repository repository, MailAddress address, ProxyMail proxy) { 48 | when(repository.getProxy(any(MailAddress.class))).thenAnswer(invocationOnMock -> { 49 | MailAddress passedAddress = (MailAddress) invocationOnMock.getArguments()[0]; 50 | if (address.toString().equals(passedAddress.toString())) { 51 | return Optional.of(proxy); 52 | } 53 | return Optional.empty(); 54 | }); 55 | } 56 | 57 | public static void whenAddressExistsReturn(Repository repository, MailAddress address, boolean exists) { 58 | when(repository.proxyExists(any(MailAddress.class))).thenAnswer(invocationOnMock -> { 59 | MailAddress passedAddress = (MailAddress) invocationOnMock.getArguments()[0]; 60 | if (address.toString().equals(passedAddress.toString())) { 61 | return exists; 62 | } 63 | throw new IllegalStateException(format("#proxyExists called with %s but %s was expected", passedAddress, address)); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /forward-cat-website/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | application.secret="iDsr0 5 | @{ 6 | if(lang.code == "en") { 7 |

How long will my temporary email address be active for?

8 |

9 | Your temporary address will be initially active for a week. A day before it expires you'll receive an 10 | email allowing you to extend it for another week, but you can only do it once. 11 |

12 |

Can I deactivate my temporary email address?

13 |

14 | Yes. In the activation email you received when you created your temporary email address there is a link 15 | to deactivate it whenever you want. 16 |

17 |

I've got another question!

18 |

19 | Feel free to contact us, we'll answer as soon as possible. 20 |

21 | 22 | } else if(lang.code == "ca") { 23 |

Quant de temps estarà activa la meva adreça temporal?

24 |

25 | La teva adreça temporal estarà activa durant una setmana. Un dia abans de que es desactivi rebràs un 26 | correu que et permetrà extendre-la una setmana més, però només es pot extendre un cop. 27 |

28 |

Puc desactivar la meva adreça temporal?

29 |

30 | Si. En el correu d'activació que vas rebre quan vas crear la teva adreça temporal hi ha un enllaç des 31 | d'on pots desactivar-la quan tu vulguis. 32 |

33 |

Tinc una altra pregunta!

34 |

35 | Posa't en contacte amb nosaltres, respondrem el més aviat possible. 36 |

37 | 38 | } else if(lang.code == "es") { 39 |

Quanto tiempo estará activa mi dirección temporal?

40 |

41 | Tu dirección de correo temporal estará activa durante una semana. Un dia antes de que se desactive 42 | recibirás un correo que te pemitirá extenderla una semana más, pero solamente se puede extender una vez. 43 |

44 |

Puedo desactivar mi dirección temporal?

45 |

46 | Sí. En el correo de activación que recibiste cuando creaste tu dirección temporal hay un enlace des del 47 | que puedes desactivarla cuando quieras. 48 |

49 |

Tengo otra pregunta!

50 |

51 | Ponte en contacto con nosotros, responderemos lo más pronto posible. 52 |

53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /forward-cat-website/app/assets/javascripts/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var proxyForm = $('#proxy-form'); 3 | proxyForm.validate({ 4 | rules: { 5 | email: { 6 | required: true, 7 | email: true 8 | }, 9 | proxy: { 10 | required: true, 11 | minlength: 3, 12 | remote: "/validate" 13 | } 14 | }, 15 | messages: { 16 | email: { 17 | required: "Please enter a valid email address", 18 | email: "Please enter a valid email address" 19 | }, 20 | proxy: { 21 | required: "Choose an address", 22 | minlength: jQuery.validator.format("Enter at least {0} characters"), 23 | remote: jQuery.validator.format("'{0}@forward.cat' is not valid or already in use") 24 | } 25 | }, 26 | errorPlacement: function (error, element) { 27 | error.appendTo(element.parent().find(".help-info")); 28 | }, 29 | submitHandler: function (form) { 30 | var userEmail = $('#proxy-form').find('#email').val(); 31 | createCookie("user_email", userEmail, 365); 32 | form.submit(); 33 | }, 34 | success: function (label) { 35 | label.remove(); 36 | } 37 | }); 38 | 39 | $('[data-toggle="popover"]').popover(); 40 | 41 | var userEmail = readCookie("user_email"); 42 | if (userEmail !== null) { 43 | proxyForm.find('#email').val(userEmail); 44 | } 45 | 46 | $('#report-form').validate({ 47 | rules: { 48 | proxy: { 49 | required: true, 50 | minlength: 3, 51 | email: true 52 | }, 53 | message: { 54 | required: true 55 | } 56 | }, 57 | messages: { 58 | email: { 59 | required: "Please enter a valid proxy address", 60 | email: "Please enter a valid proxy address" 61 | }, 62 | message: { 63 | required: "Required" 64 | } 65 | }, 66 | errorPlacement: function (error, element) { 67 | error.appendTo(element.parent().find(".help-info")); 68 | }, 69 | submitHandler: function (form) { 70 | form.submit(); 71 | }, 72 | success: function (label) { 73 | label.remove(); 74 | } 75 | }); 76 | }); 77 | 78 | function createCookie(name, value, days) { 79 | var expires = ""; 80 | if (days) { 81 | var date = new Date(); 82 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 83 | expires = "; expires=" + date.toGMTString(); 84 | } 85 | document.cookie = name + "=" + value + expires + "; path=/"; 86 | } 87 | 88 | function readCookie(name) { 89 | var nameEQ = name + "="; 90 | var ca = document.cookie.split(';'); 91 | for (var i = 0; i < ca.length; i++) { 92 | var c = ca[i]; 93 | while (c.charAt(0) === ' ') c = c.substring(1, c.length); 94 | if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 95 | } 96 | return null; 97 | } 98 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/ConfirmProxy.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.forwardcat.common.RedisKeys; 5 | import com.google.inject.Inject; 6 | import models.Repository; 7 | import models.SpamCatcher; 8 | import models.StatsRepository; 9 | import org.apache.mailet.MailAddress; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import play.mvc.Controller; 13 | import play.mvc.Result; 14 | import play.mvc.With; 15 | import views.html.error_page; 16 | import views.html.proxy_created; 17 | 18 | import java.util.Optional; 19 | 20 | import static java.util.Optional.empty; 21 | import static models.ControllerUtils.isAuthenticated; 22 | import static models.ControllerUtils.toMailAddress; 23 | import static models.ExpirationUtils.formatInstant; 24 | 25 | @With(RedirectAction.class) 26 | public class ConfirmProxy extends Controller { 27 | 28 | private static final Logger LOGGER = LoggerFactory.getLogger(ConfirmProxy.class.getName()); 29 | private final StatsRepository statsRepo; 30 | private final Repository repository; 31 | private final SpamCatcher spamCatcher; 32 | 33 | @Inject 34 | ConfirmProxy(StatsRepository statsRepo, Repository repository, SpamCatcher spamCatcher) { 35 | this.statsRepo = statsRepo; 36 | this.repository = repository; 37 | this.spamCatcher = spamCatcher; 38 | } 39 | 40 | public Result confirm(String langCode, String p, String h) throws Exception { 41 | // Checking params 42 | Optional maybeProxyMail = toMailAddress(p); 43 | if (!maybeProxyMail.isPresent() || h == null) { 44 | return badRequest(error_page.render(lang(), empty())); 45 | } 46 | 47 | // Getting the proxy & checking that the hash is correct 48 | MailAddress proxyAddress = maybeProxyMail.get(); 49 | Optional maybeProxy = repository.getProxy(proxyAddress); 50 | if (!isAuthenticated(maybeProxy, h)) { 51 | return badRequest(error_page.render(lang(), empty())); 52 | } 53 | 54 | // Checking that the proxy is not already active 55 | ProxyMail proxy = maybeProxy.get(); 56 | if (proxy.isActive()) { 57 | LOGGER.debug("Proxy {} is already active", proxy); 58 | return badRequest(); 59 | } 60 | proxy.activate(); 61 | 62 | if (spamCatcher.isSpam(proxyAddress)) { 63 | proxy.block(); 64 | statsRepo.incrementCounter(RedisKeys.SPAMMER_PROXIES_BLOCKED_COUNTER); 65 | } 66 | 67 | repository.save(proxy); 68 | statsRepo.incrementCounter(RedisKeys.PROXIES_ACTIVATED_COUNTER); 69 | 70 | // Generating the response 71 | String expirationDate = formatInstant(proxy.getExpirationTime(), lang()); 72 | return ok(proxy_created.render(lang(), proxyAddress, expirationDate)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 2.2.3 8 | 9 | 10 | 11 | com.forwardcat 12 | parent 13 | 1.0-SNAPSHOT 14 | 15 | common 16 | 1.0-SNAPSHOT 17 | Forward Cat - Common 18 | 19 | 20 | 21 | 22 | org.apache.james 23 | apache-mailet-api 24 | 2.5.1-SNAPSHOT 25 | provided 26 | 27 | 28 | 29 | javax.mail 30 | mail 31 | 1.4 32 | 33 | 34 | 36 | com.google.guava 37 | guava 38 | 13.0.1 39 | 40 | 41 | 42 | 43 | 44 | junit 45 | junit 46 | 4.11 47 | test 48 | 49 | 50 | 51 | org.mockito 52 | mockito-all 53 | 1.9.5 54 | test 55 | 56 | 57 | 58 | org.hamcrest 59 | hamcrest-all 60 | 1.3 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.avaje.ebeanorm 69 | avaje-ebeanorm-mavenenhancer 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/simple_email.scala.html: -------------------------------------------------------------------------------- 1 | @(lang: Lang, title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | Forward Cat - @title 8 | 38 | 39 | 40 | @**@ 41 | 42 | 43 | 48 | 49 |
44 | @* 45 | Your alt text*@ 46 | @content 47 |
50 | @**@ 51 | 52 | -------------------------------------------------------------------------------- /forward-cat-website/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | GET / @controllers.Landing.index(langCode = "en") 6 | GET /$langCode<[a-z]{2}>/ @controllers.Landing.index(langCode) 7 | GET /faq @controllers.Faq.get(langCode = "en") 8 | GET /$langCode<[a-z]{2}>/faq @controllers.Faq.get(langCode) 9 | GET /report @controllers.Report.reportGet(langCode = "en") 10 | GET /$langCode<[a-z]{2}>/report @controllers.Report.reportGet(langCode) 11 | GET /report-user @controllers.Report.reportUser(langCode = "en", proxy: String, message: String) 12 | GET /$langCode<[a-z]{2}>/report-user @controllers.Report.reportUser(langCode, proxy: String, message: String) 13 | GET /contact @controllers.Contact.contactGet(langCode = "en") 14 | GET /$langCode<[a-z]{2}>/contact @controllers.Contact.contactGet(langCode) 15 | GET /contact-sent @controllers.Contact.contactSent(langCode = "en", email: String, message: String) 16 | GET /$langCode<[a-z]{2}>/contact-sent @controllers.Contact.contactSent(langCode, email: String, message: String) 17 | GET /confirm @controllers.ConfirmProxy.confirm(langCode = "en", p: String, h: String) 18 | GET /$langCode<[a-z]{2}>/confirm @controllers.ConfirmProxy.confirm(langCode, p: String, h: String) 19 | GET /add @controllers.AddProxy.addProxy(langCode = "en", proxy: String, email: String) 20 | GET /$langCode<[a-z]{2}>/add @controllers.AddProxy.addProxy(langCode, proxy: String, email: String) 21 | GET /confirm-deletion @controllers.DeleteProxy.confirmDeletion(langCode = "en", p: String, h: String) 22 | GET /$langCode<[a-z]{2}>/confirm-deletion @controllers.DeleteProxy.confirmDeletion(langCode, p: String, h: String) 23 | GET /delete @controllers.DeleteProxy.delete(langCode = "en", p: String, h: String) 24 | GET /$langCode<[a-z]{2}>/delete @controllers.DeleteProxy.delete(langCode, p: String, h: String) 25 | GET /extend @controllers.ExtendProxy.extend(langCode = "en", p: String, h: String) 26 | GET /$langCode<[a-z]{2}>/extend @controllers.ExtendProxy.extend(langCode, p: String, h: String) 27 | GET /stats @controllers.Stats.render() 28 | GET /validate @controllers.ValidateProxy.validate(proxy: String) 29 | 30 | # Map static resources from the /public folder to the /assets URL path 31 | GET /assets/*file controllers.Assets.at(path="/public", file) 32 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/ExtendProxy.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.Inject; 5 | import models.Repository; 6 | import org.apache.mailet.MailAddress; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import play.mvc.Controller; 10 | import play.mvc.Result; 11 | import play.mvc.With; 12 | import views.html.error_page; 13 | import views.html.proxy_extended; 14 | 15 | import java.time.ZonedDateTime; 16 | import java.util.Date; 17 | import java.util.Optional; 18 | 19 | import static java.util.Optional.empty; 20 | import static models.ControllerUtils.isAuthenticated; 21 | import static models.ControllerUtils.toMailAddress; 22 | import static models.ExpirationUtils.*; 23 | 24 | @With(RedirectAction.class) 25 | public class ExtendProxy extends Controller { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(ExtendProxy.class.getName()); 28 | private final Repository repository; 29 | 30 | @Inject 31 | ExtendProxy(Repository repository) { 32 | this.repository = repository; 33 | } 34 | 35 | public Result extend(String langCode, String p, String h) { 36 | // Checking params 37 | Optional maybeProxyMail = toMailAddress(p); 38 | if (!maybeProxyMail.isPresent() || h == null) { 39 | return badRequest(error_page.render(lang(), empty())); 40 | } 41 | 42 | // Getting the proxy & checking that the hash is correct 43 | MailAddress proxyMail = maybeProxyMail.get(); 44 | Optional maybeProxy = repository.getProxy(proxyMail); 45 | if (!isAuthenticated(maybeProxy, h)) { 46 | return badRequest(error_page.render(lang(), empty())); 47 | } 48 | 49 | // Checking that the proxy is active 50 | ProxyMail proxy = maybeProxy.get(); 51 | if (!proxy.isActive()) { 52 | LOGGER.debug("Proxy {} is already active", proxy); 53 | return badRequest(); 54 | } 55 | 56 | // Checking that the proxy has not been active for more than 15 days 57 | 58 | ZonedDateTime maxExpirationTime = toZonedDateTime(proxy.getCreationTime()).plusDays(getMaxProxyDuration()); 59 | ZonedDateTime newExpirationTime = getNewExpirationTime(toZonedDateTime(proxy.getExpirationTime()).plusDays(getIncrementDaysAdded()), maxExpirationTime); 60 | 61 | Date expirationTimeDate = toDate(newExpirationTime); 62 | proxy.setExpirationTime(expirationTimeDate); 63 | 64 | repository.update(proxy); 65 | 66 | // Generating the answer 67 | String date = formatInstant(expirationTimeDate, lang()); 68 | return ok(proxy_extended.render(lang(), proxyMail, date)); 69 | } 70 | 71 | private ZonedDateTime getNewExpirationTime(ZonedDateTime newExpirationTime, ZonedDateTime maxExpirationTime) { 72 | if (newExpirationTime.isAfter(maxExpirationTime)) { 73 | return maxExpirationTime; 74 | } 75 | return newExpirationTime; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ExtendProxyTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.AbstractModule; 5 | import models.Repository; 6 | import org.apache.mailet.MailAddress; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.Mock; 10 | import org.mockito.runners.MockitoJUnitRunner; 11 | import play.mvc.Result; 12 | import play.test.FakeRequest; 13 | 14 | import java.io.IOException; 15 | 16 | import static controllers.TestUtils.*; 17 | import static models.ControllerUtils.getHash; 18 | import static org.hamcrest.Matchers.is; 19 | import static org.junit.Assert.assertThat; 20 | import static org.mockito.Matchers.any; 21 | import static org.mockito.Mockito.verify; 22 | import static play.test.Helpers.*; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class ExtendProxyTest extends PlayTest { 26 | 27 | MailAddress proxyMail = toMailAddress("test@forward.cat"); 28 | ProxyMail proxy = activeProxy(proxyMail, false); 29 | String proxyHash = getHash(proxy); 30 | 31 | @Mock Repository repository; 32 | 33 | @Override 34 | public AbstractModule getModule() throws IOException { 35 | whenAddressReturnProxy(repository, proxyMail, proxy); 36 | 37 | return new AbstractModule() { 38 | @Override 39 | protected void configure() { 40 | bind(Repository.class).toInstance(repository); 41 | } 42 | }; 43 | } 44 | 45 | @Test 46 | public void proxyMailMissing_sendBadRequest() throws Exception { 47 | Result route = route(request(null, proxyHash)); 48 | assertThat(status(route), is(BAD_REQUEST)); 49 | } 50 | 51 | @Test 52 | public void hashMissing_sendBadRequest() throws Exception { 53 | Result route = route(request(proxyMail.toString(), null)); 54 | assertThat(status(route), is(BAD_REQUEST)); 55 | } 56 | 57 | @Test 58 | public void proxyNonExisting_sendBadRequest() throws Exception { 59 | Result route = route(request("nonValid@forward.cat", proxyHash)); 60 | assertThat(status(route), is(BAD_REQUEST)); 61 | } 62 | 63 | @Test 64 | public void invalidHash_sendBadRequest() throws Exception { 65 | Result route = route(request(proxyMail.toString(), "wrongHash")); 66 | assertThat(status(route), is(BAD_REQUEST)); 67 | } 68 | 69 | @Test 70 | public void everythingFine_sendConfirmationPage() throws Exception { 71 | Result route = route(request(proxyMail.toString(), proxyHash)); 72 | assertThat(status(route), is(OK)); 73 | verify(repository).update(any(ProxyMail.class)); 74 | } 75 | 76 | private FakeRequest request(String proxy, String hash) { 77 | if (proxy == null) { 78 | return fakeRequest(GET, "/extend?h=" + hash); 79 | } else if (hash == null) { 80 | return fakeRequest(GET, "/extend?p=" + proxy); 81 | } 82 | return fakeRequest(GET, "/extend?p=" + proxy + "&h=" + hash); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/resources/domainlist.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | forward.cat 59 | 60 | true 61 | true 62 | localhost 63 | 64 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ValidateProxyTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.inject.AbstractModule; 4 | import models.Repository; 5 | import org.apache.mailet.MailAddress; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | import play.mvc.Result; 11 | import play.test.FakeRequest; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.hamcrest.Matchers.is; 16 | import static org.junit.Assert.assertThat; 17 | import static org.mockito.Matchers.any; 18 | import static org.mockito.Mockito.when; 19 | import static play.test.Helpers.*; 20 | 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class ValidateProxyTest extends PlayTest { 23 | 24 | public static final String USER_IN_USE = "in-use"; 25 | public static final String USER_NOT_IN_USE = "not-in-use"; 26 | @Mock Repository repository; 27 | 28 | @Override 29 | public AbstractModule getModule() throws IOException { 30 | when(repository.proxyExists(any(MailAddress.class))).thenAnswer(invocationOnMock -> { 31 | MailAddress passedAddress = (MailAddress) invocationOnMock.getArguments()[0]; 32 | return (USER_IN_USE + "@forward.cat").equals(passedAddress.toString()); 33 | }); 34 | 35 | return new AbstractModule() { 36 | @Override 37 | protected void configure() { 38 | bind(Repository.class).toInstance(repository); 39 | } 40 | }; 41 | } 42 | 43 | @Test 44 | public void noProxyParam_sendInvalidUsername() throws Exception { 45 | Result route = route(request("")); 46 | assertThatIsInvalid(route); 47 | } 48 | 49 | @Test 50 | public void incorrectProxyParam_sendInvalidUsername() throws Exception { 51 | Result route = route(request("has space")); 52 | assertThatIsInvalid(route); 53 | } 54 | 55 | @Test(expected = RuntimeException.class) 56 | public void redisConnectionError_sendInvalidUsername() throws Exception { 57 | when(repository.proxyExists(any(MailAddress.class))).thenThrow(new RuntimeException()); 58 | Result route = route(request(USER_NOT_IN_USE)); 59 | assertThatIsInvalid(route); 60 | } 61 | 62 | @Test 63 | public void usernameInUse_sendInvalidUsername() throws Exception { 64 | Result route = route(request(USER_IN_USE)); 65 | assertThatIsInvalid(route); 66 | } 67 | 68 | @Test 69 | public void usernameFree_sendValidUsername() throws Exception { 70 | Result route = route(request(USER_NOT_IN_USE)); 71 | assertThatIsValid(route); 72 | } 73 | 74 | private FakeRequest request(String proxy) { 75 | if (proxy == null) { 76 | return fakeRequest(GET, "/validate"); 77 | } 78 | return fakeRequest(GET, "/validate?proxy=" + proxy); 79 | } 80 | 81 | private void assertThatIsValid(Result route) { 82 | assertThat(status(route), is(OK)); 83 | assertThat(contentAsString(route), is("true")); 84 | } 85 | 86 | private void assertThatIsInvalid(Result route) { 87 | assertThat(status(route), is(OK)); 88 | assertThat(contentAsString(route), is("false")); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/Options.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import redis.clients.jedis.Protocol; 4 | 5 | /** 6 | * models.Options for the Server 7 | */ 8 | public class Options { 9 | 10 | // Redis models.Options 11 | private String redisHost = "localhost"; 12 | 13 | private int redisPort = Protocol.DEFAULT_PORT; 14 | 15 | private int maxActiveConnections = Runtime.getRuntime().availableProcessors() * 4; 16 | 17 | private int maxIdleConnections = maxActiveConnections / 2; 18 | 19 | private int minIdleConnections = 1; 20 | 21 | private int numTestsPerEvictionRun = maxActiveConnections; 22 | 23 | private boolean testConnectionWhileIdle = true; 24 | 25 | private boolean testConnectionOnBorrow = true; 26 | 27 | private boolean testConnectionOnReturn = false; 28 | 29 | private long timeBetweenEvictionRunsMillis = 60000; 30 | 31 | private int minutesBetweenAlerts = 5; 32 | 33 | private int timeBetweenAlertMailsMillis = 1000; 34 | 35 | public String getRedisHost() { 36 | return redisHost; 37 | } 38 | 39 | public int getRedisPort() { 40 | return redisPort; 41 | } 42 | 43 | public int getMaxActiveConnections() { 44 | return maxActiveConnections; 45 | } 46 | 47 | public int getMaxIdleConnections() { 48 | return maxIdleConnections; 49 | } 50 | 51 | public int getMinIdleConnections() { 52 | return minIdleConnections; 53 | } 54 | 55 | public int getNumTestsPerEvictionRun() { 56 | return numTestsPerEvictionRun; 57 | } 58 | 59 | public boolean isTestConnectionWhileIdle() { 60 | return testConnectionWhileIdle; 61 | } 62 | 63 | public boolean isTestConnectionOnBorrow() { 64 | return testConnectionOnBorrow; 65 | } 66 | 67 | public boolean isTestConnectionOnReturn() { 68 | return testConnectionOnReturn; 69 | } 70 | 71 | public long getTimeBetweenEvictionRunsMillis() { 72 | return timeBetweenEvictionRunsMillis; 73 | } 74 | 75 | public int getMinutesBetweenAlerts() { 76 | return minutesBetweenAlerts; 77 | } 78 | 79 | public int getTimeBetweenAlertMailsMillis() { 80 | return timeBetweenAlertMailsMillis; 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return "Options{" + 86 | "redisHost='" + redisHost + '\'' + 87 | ", redisPort=" + redisPort + 88 | ", maxActiveConnections=" + maxActiveConnections + 89 | ", maxIdleConnections=" + maxIdleConnections + 90 | ", minIdleConnections=" + minIdleConnections + 91 | ", numTestsPerEvictionRun=" + numTestsPerEvictionRun + 92 | ", testConnectionWhileIdle=" + testConnectionWhileIdle + 93 | ", testConnectionOnBorrow=" + testConnectionOnBorrow + 94 | ", testConnectionOnReturn=" + testConnectionOnReturn + 95 | ", timeBetweenEvictionRunsMillis=" + timeBetweenEvictionRunsMillis + 96 | ", minutesBetweenAlerts=" + minutesBetweenAlerts + 97 | ", timeBetweenAlertMailsMillis=" + timeBetweenAlertMailsMillis + 98 | '}'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/DeleteProxy.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.Inject; 5 | import models.Repository; 6 | import org.apache.mailet.MailAddress; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import play.mvc.Controller; 10 | import play.mvc.Result; 11 | import play.mvc.With; 12 | import views.html.confirm_deletion; 13 | import views.html.error_page; 14 | import views.html.proxy_deleted; 15 | 16 | import java.util.Optional; 17 | 18 | import static java.util.Optional.empty; 19 | import static models.ControllerUtils.*; 20 | import static models.ExpirationUtils.formatInstant; 21 | 22 | @With(RedirectAction.class) 23 | public class DeleteProxy extends Controller { 24 | 25 | private static final Logger LOGGER = LoggerFactory.getLogger(DeleteProxy.class.getName()); 26 | private final Repository repository; 27 | 28 | @Inject 29 | DeleteProxy(Repository repository) { 30 | this.repository = repository; 31 | } 32 | 33 | public Result confirmDeletion(String langCode, String p, String h) { 34 | // Checking params 35 | Optional maybeProxyMail = toMailAddress(p); 36 | if (!maybeProxyMail.isPresent() || h == null) { 37 | return badRequest(error_page.render(lang(), empty())); 38 | } 39 | 40 | // Getting the proxy & checking that the hash is correct 41 | MailAddress proxyMail = maybeProxyMail.get(); 42 | Optional maybeProxy = repository.getProxy(proxyMail); 43 | if (!isAuthenticated(maybeProxy, h)) { 44 | return badRequest(error_page.render(lang(), empty())); 45 | } 46 | 47 | // Checking that the proxy is active 48 | ProxyMail proxy = maybeProxy.get(); 49 | if (!proxy.isActive()) { 50 | LOGGER.debug("Proxy {} is not active", proxy); 51 | return badRequest(); 52 | } 53 | 54 | // Generating the answer 55 | String expirationDate = formatInstant(proxy.getExpirationTime(), lang()); 56 | String hashValue = getHash(proxy); 57 | return ok(confirm_deletion.render(lang(), proxyMail, expirationDate, hashValue)); 58 | } 59 | 60 | public Result delete(String langCode, String p, String h) { 61 | // Checking params 62 | Optional maybeProxyMail = toMailAddress(p); 63 | if (!maybeProxyMail.isPresent() || h == null) { 64 | return badRequest(error_page.render(lang(), empty())); 65 | } 66 | 67 | // Getting the proxy & checking that the hash is correct 68 | MailAddress proxyMail = maybeProxyMail.get(); 69 | Optional maybeProxy = repository.getProxy(proxyMail); 70 | if (!isAuthenticated(maybeProxy, h)) { 71 | return badRequest(error_page.render(lang(), empty())); 72 | } 73 | 74 | // Checking whether the proxy is active or not 75 | ProxyMail proxy = maybeProxy.get(); 76 | if (!proxy.isActive()) { 77 | LOGGER.debug("Proxy is not active"); 78 | return badRequest(); 79 | } 80 | 81 | // Removing the proxy 82 | repository.delete(proxy); 83 | 84 | // Sending the response 85 | return ok(proxy_deleted.render(lang())); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ConfirmDeletionTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.AbstractModule; 5 | import models.Repository; 6 | import org.apache.mailet.MailAddress; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.Mock; 10 | import org.mockito.runners.MockitoJUnitRunner; 11 | import play.mvc.Result; 12 | import play.test.FakeRequest; 13 | 14 | import java.io.IOException; 15 | 16 | import static controllers.TestUtils.*; 17 | import static models.ControllerUtils.getHash; 18 | import static org.hamcrest.Matchers.is; 19 | import static org.junit.Assert.assertThat; 20 | import static org.mockito.Mockito.when; 21 | import static play.mvc.Http.Status.BAD_REQUEST; 22 | import static play.test.Helpers.*; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class ConfirmDeletionTest extends PlayTest { 26 | 27 | MailAddress proxyMail = toMailAddress("test@forward.cat"); 28 | ProxyMail proxy = activeProxy(proxyMail, false); 29 | String proxyHash = getHash(proxy); 30 | 31 | @Mock Repository repository; 32 | 33 | @Override 34 | public AbstractModule getModule() throws IOException { 35 | whenAddressReturnProxy(repository, proxyMail, proxy); 36 | 37 | return new AbstractModule() { 38 | @Override 39 | protected void configure() { 40 | bind(Repository.class).toInstance(repository); 41 | } 42 | }; 43 | } 44 | 45 | @Test 46 | public void proxyMailMissing_sendBadRequest() throws Exception { 47 | Result route = route(request(null, proxyHash)); 48 | assertThat(status(route), is(BAD_REQUEST)); 49 | } 50 | 51 | @Test 52 | public void hashMissing_sendBadRequest() throws Exception { 53 | Result route = route(request(proxyMail.toString(), null)); 54 | assertThat(status(route), is(BAD_REQUEST)); 55 | } 56 | 57 | @Test 58 | public void proxyNonExisting_sendBadRequest() throws Exception { 59 | Result route = route(request("nonValid@forward.cat", proxyHash)); 60 | assertThat(status(route), is(BAD_REQUEST)); 61 | } 62 | 63 | @Test 64 | public void invalidHash_sendBadRequest() throws Exception { 65 | Result route = route(request(proxyMail.toString(), "wrongHash")); 66 | assertThat(status(route), is(BAD_REQUEST)); 67 | } 68 | 69 | @Test(expected = RuntimeException.class) 70 | public void redisException_sendBadRequest() throws Exception { 71 | when(repository.getProxy(proxyMail)).thenThrow(new RuntimeException()); 72 | Result route = route(request(proxyMail.toString(), proxyHash)); 73 | assertThat(status(route), is(BAD_REQUEST)); 74 | } 75 | 76 | @Test 77 | public void everythingFine_sendConfirmationPage() throws Exception { 78 | Result route = route(request(proxyMail.toString(), proxyHash)); 79 | assertThat(status(route), is(OK)); 80 | } 81 | 82 | private FakeRequest request(String proxy, String hash) { 83 | if (proxy == null) { 84 | return fakeRequest(GET, "/confirm-deletion?h=" + hash); 85 | } else if (hash == null) { 86 | return fakeRequest(GET, "/confirm-deletion?p=" + proxy); 87 | } 88 | return fakeRequest(GET, "/confirm-deletion?p=" + proxy + "&h=" + hash); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ConfirmProxyTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.forwardcat.common.RedisKeys; 5 | import com.google.inject.AbstractModule; 6 | import models.Repository; 7 | import models.StatsRepository; 8 | import org.apache.mailet.MailAddress; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.runners.MockitoJUnitRunner; 13 | import play.mvc.Result; 14 | import play.test.FakeRequest; 15 | 16 | import javax.mail.internet.AddressException; 17 | import java.io.IOException; 18 | 19 | import static controllers.TestUtils.*; 20 | import static models.ControllerUtils.getHash; 21 | import static org.hamcrest.Matchers.is; 22 | import static org.junit.Assert.assertThat; 23 | import static org.mockito.Mockito.verify; 24 | import static play.mvc.Http.Status.BAD_REQUEST; 25 | import static play.test.Helpers.*; 26 | 27 | @RunWith(MockitoJUnitRunner.class) 28 | public class ConfirmProxyTest extends PlayTest { 29 | 30 | MailAddress proxyMail = toMailAddress("test@forward.cat"); 31 | ProxyMail proxy = inactiveProxy(proxyMail); 32 | String proxyHash = getHash(proxy); 33 | 34 | @Mock Repository repository; 35 | @Mock StatsRepository statsRepo; 36 | 37 | @Override 38 | public AbstractModule getModule() throws IOException, AddressException { 39 | whenAddressReturnProxy(repository, proxyMail, proxy); 40 | 41 | return new AbstractModule() { 42 | @Override 43 | protected void configure() { 44 | bind(Repository.class).toInstance(repository); 45 | bind(StatsRepository.class).toInstance(statsRepo); 46 | } 47 | }; 48 | } 49 | 50 | @Test 51 | public void proxyMailMissing_sendBadRequest() throws Exception { 52 | Result route = route(request(null, proxyHash)); 53 | assertThat(status(route), is(BAD_REQUEST)); 54 | } 55 | 56 | @Test 57 | public void hashMissing_sendBadRequest() throws Exception { 58 | Result route = route(request(proxyMail.toString(), null)); 59 | assertThat(status(route), is(BAD_REQUEST)); 60 | } 61 | 62 | @Test 63 | public void proxyNonExisting_sendBadRequest() throws Exception { 64 | Result route = route(request("nonValid@forward.cat", proxyHash)); 65 | assertThat(status(route), is(BAD_REQUEST)); 66 | } 67 | 68 | @Test 69 | public void invalidHash_sendBadRequest() throws Exception { 70 | Result route = route(request(proxyMail.toString(), "wrongHash")); 71 | assertThat(status(route), is(BAD_REQUEST)); 72 | } 73 | 74 | @Test 75 | public void everythingFine_sendConfirmationPage() throws Exception { 76 | Result route = route(request(proxyMail.toString(), proxyHash)); 77 | assertThat(status(route), is(OK)); 78 | verify(repository).save(proxy); 79 | verify(statsRepo).incrementCounter(RedisKeys.PROXIES_ACTIVATED_COUNTER); 80 | } 81 | 82 | private FakeRequest request(String proxy, String hash) { 83 | if (proxy == null) { 84 | return fakeRequest(GET, "/confirm?h=" + hash); 85 | } else if (hash == null) { 86 | return fakeRequest(GET, "/confirm?p=" + proxy); 87 | } 88 | return fakeRequest(GET, "/confirm?p=" + proxy + "&h=" + hash); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /forward-cat-website/app/alerts/SendAlertJob.java: -------------------------------------------------------------------------------- 1 | package alerts; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.Inject; 5 | import models.MailSender; 6 | import models.Options; 7 | import models.Repository; 8 | import org.apache.mailet.MailAddress; 9 | import org.quartz.Job; 10 | import org.quartz.JobExecutionContext; 11 | import org.quartz.JobExecutionException; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import play.i18n.Lang; 15 | import play.twirl.api.Html; 16 | import views.html.proxy_expiring_email; 17 | 18 | import java.util.Date; 19 | import java.util.Set; 20 | 21 | import static models.ControllerUtils.getHash; 22 | import static models.ControllerUtils.toMailAddress; 23 | import static models.ExpirationUtils.formatInstant; 24 | 25 | /** 26 | * Job that sends alerts to the users of proxies before they expire in order to 27 | * allow them to extend its life. 28 | */ 29 | public class SendAlertJob implements Job { 30 | 31 | private static final Logger LOGGER = LoggerFactory.getLogger(SendAlertJob.class.getName()); 32 | private final String subject = "Forward Cat"; 33 | private final MailSender mailSender; 34 | private final Options options; 35 | private final Repository repository; 36 | 37 | @Inject 38 | SendAlertJob(Repository repository, MailSender mailSender, 39 | Options options) { 40 | this.repository = repository; 41 | this.mailSender = mailSender; 42 | this.options = options; 43 | } 44 | 45 | @Override 46 | public void execute(JobExecutionContext context) throws JobExecutionException { 47 | // Getting the proxies that will expire before tomorrow 48 | Set expiringProxies = repository.getExpiringProxies(); 49 | 50 | sendAlerts(expiringProxies); 51 | repository.removeExpiredProxies(); 52 | repository.removeNonActivatedProxies(); 53 | repository.removeUsersWithoutProxies(); 54 | } 55 | 56 | /** 57 | * Send email alerts to the given proxy users and mark is at notified 58 | * 59 | * @param proxies 60 | */ 61 | private void sendAlerts(Set proxies) { 62 | for (ProxyMail proxy : proxies) { 63 | try { 64 | // Sending the mail 65 | Lang lang = new Lang(Lang.get(proxy.getLang()).get()); 66 | MailAddress address = toMailAddress(proxy.getUser().getEmailAddress()).get(); 67 | 68 | Date expirationTime = proxy.getExpirationTime(); 69 | String date = formatInstant(expirationTime, lang); 70 | 71 | Html content = proxy_expiring_email.render(lang, proxy.getProxyAddress(), date, getHash(proxy)); 72 | mailSender.sendHtmlMail(address, subject, content.toString()); 73 | 74 | proxy.setExpirationNotified(true); 75 | repository.update(proxy); 76 | } catch (Exception ex) { 77 | LOGGER.error("Unexpected exception sending expiration notification for proxy: " + proxy, ex); 78 | } 79 | 80 | // Wait before sending another email 81 | try { 82 | Thread.sleep(options.getTimeBetweenAlertMailsMillis()); 83 | } catch (InterruptedException e) { 84 | LOGGER.error("Unexpected error", e); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/DeleteProxyTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.AbstractModule; 5 | import models.Repository; 6 | import org.apache.mailet.MailAddress; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.Mock; 10 | import org.mockito.runners.MockitoJUnitRunner; 11 | import play.mvc.Result; 12 | import play.test.FakeRequest; 13 | 14 | import java.io.IOException; 15 | 16 | import static controllers.TestUtils.*; 17 | import static models.ControllerUtils.getHash; 18 | import static org.hamcrest.Matchers.is; 19 | import static org.junit.Assert.assertThat; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | import static play.mvc.Http.Status.BAD_REQUEST; 23 | import static play.test.Helpers.*; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class DeleteProxyTest extends PlayTest { 27 | 28 | MailAddress proxyMail = toMailAddress("test@forward.cat"); 29 | ProxyMail proxy = activeProxy(proxyMail, false); 30 | String proxyHash = getHash(proxy); 31 | 32 | @Mock Repository repository; 33 | 34 | @Override 35 | public AbstractModule getModule() throws IOException { 36 | whenAddressReturnProxy(repository, proxyMail, proxy); 37 | 38 | return new AbstractModule() { 39 | @Override 40 | protected void configure() { 41 | bind(Repository.class).toInstance(repository); 42 | } 43 | }; 44 | } 45 | 46 | @Test 47 | public void proxyMailMissing_sendBadRequest() throws Exception { 48 | Result route = route(request(null, proxyHash)); 49 | assertThat(status(route), is(BAD_REQUEST)); 50 | } 51 | 52 | @Test 53 | public void hashMissing_sendBadRequest() throws Exception { 54 | Result route = route(request(proxyMail.toString(), null)); 55 | assertThat(status(route), is(BAD_REQUEST)); 56 | } 57 | 58 | @Test 59 | public void proxyNonExisting_sendBadRequest() throws Exception { 60 | Result route = route(request("nonValid@forward.cat", proxyHash)); 61 | assertThat(status(route), is(BAD_REQUEST)); 62 | } 63 | 64 | @Test 65 | public void invalidHash_sendBadRequest() throws Exception { 66 | Result route = route(request(proxyMail.toString(), "wrongHash")); 67 | assertThat(status(route), is(BAD_REQUEST)); 68 | } 69 | 70 | @Test(expected = RuntimeException.class) 71 | public void redisError_sendInternalError() throws Exception { 72 | when(repository.getProxy(proxyMail)).thenThrow(new RuntimeException()); 73 | Result route = route(request(proxyMail.toString(), proxyHash)); 74 | assertThat(status(route), is(BAD_REQUEST)); 75 | } 76 | 77 | @Test 78 | public void everythingFine_sendConfirmationPage() throws Exception { 79 | Result route = route(request(proxyMail.toString(), proxyHash)); 80 | assertThat(status(route), is(OK)); 81 | verify(repository).delete(proxy); 82 | } 83 | 84 | private FakeRequest request(String proxy, String hash) { 85 | if (proxy == null) { 86 | return fakeRequest(GET, "/delete?h=" + hash); 87 | } else if (hash == null) { 88 | return fakeRequest(GET, "/delete?p=" + proxy); 89 | } 90 | return fakeRequest(GET, "/delete?p=" + proxy + "&h=" + hash); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /forward-cat-website/app/controllers/AddProxy.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.forwardcat.common.User; 5 | import com.google.inject.Inject; 6 | import models.MailSender; 7 | import models.Repository; 8 | import org.apache.mailet.MailAddress; 9 | import play.i18n.Messages; 10 | import play.mvc.Controller; 11 | import play.mvc.Result; 12 | import play.mvc.With; 13 | import play.twirl.api.Html; 14 | import views.html.email_sent; 15 | import views.html.error_page; 16 | import views.html.proxy_created_email; 17 | 18 | import java.time.ZonedDateTime; 19 | import java.util.Optional; 20 | 21 | import static java.util.Optional.empty; 22 | import static models.ControllerUtils.*; 23 | import static models.ExpirationUtils.now; 24 | import static models.ExpirationUtils.toDate; 25 | import static models.MailAddressNormalizer.normalize; 26 | 27 | @With(RedirectAction.class) 28 | public class AddProxy extends Controller { 29 | 30 | private static final int INITIAL_PROXY_DURATION = 7; 31 | private static final int MAX_PROXIES = 3; 32 | private final Repository repository; 33 | private final MailSender mailSender; 34 | 35 | @Inject 36 | AddProxy(Repository repository, MailSender mailSender) { 37 | this.repository = repository; 38 | this.mailSender = mailSender; 39 | } 40 | 41 | public Result addProxy(String langCode, String proxy, String email) { 42 | // Checking params 43 | Optional maybeUserMail = normalize(toMailAddress(email)); 44 | if (proxy == null || !maybeUserMail.isPresent()) { 45 | return badRequest(error_page.render(lang(), empty())); 46 | } 47 | 48 | MailAddress userMail = maybeUserMail.get(); 49 | // Don't allow chained proxies 50 | if (isLocal(userMail)) { 51 | return badRequest(error_page.render(lang(), empty())); 52 | } 53 | 54 | Optional maybeProxyMailAddress = getMailAddress(proxy); 55 | if (!maybeProxyMailAddress.isPresent()) { 56 | return badRequest(error_page.render(lang(), empty())); 57 | } 58 | 59 | MailAddress proxyMailAddress = maybeProxyMailAddress.get(); 60 | 61 | ZonedDateTime creationTime = now(); 62 | ZonedDateTime expirationTime = creationTime.plusDays(INITIAL_PROXY_DURATION); 63 | ProxyMail proxyMail = ProxyMail.create(proxyMailAddress, userMail, toDate(creationTime), toDate(expirationTime), lang().code()); 64 | 65 | // Creating the proxy 66 | if (repository.proxyExists(proxyMailAddress)) { 67 | return badRequest(error_page.render(lang(), empty())); // Proxy already exists: cannot create a new one 68 | } 69 | 70 | User user = repository.getUser(userMail).orElseGet(() -> 71 | User.create(userMail, toDate(creationTime))); 72 | 73 | if (user.getProxies().size() >= MAX_PROXIES) { 74 | return badRequest(error_page.render(lang(), Optional.of("error.too_many_proxies"))); 75 | } 76 | 77 | user.getProxies().add(proxyMail); 78 | repository.save(user); 79 | 80 | // Sending the confirmation mail 81 | String subject = Messages.get(lang(), "proxy_created_email.title"); 82 | Html content = proxy_created_email.render(lang(), proxyMailAddress, getHash(proxyMail)); 83 | mailSender.sendHtmlMail(userMail, subject, content.toString()); 84 | 85 | return ok(email_sent.render(lang())); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/RedirectActionTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Lists; 5 | import com.google.inject.AbstractModule; 6 | import org.junit.Test; 7 | import play.mvc.Http; 8 | import play.mvc.Result; 9 | import play.test.FakeRequest; 10 | 11 | import java.io.IOException; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Map.Entry; 15 | 16 | import static controllers.ContainsHtmlLang.containsHtmlLang; 17 | import static org.hamcrest.Matchers.*; 18 | import static org.junit.Assert.assertThat; 19 | import static play.mvc.Http.Status.MOVED_PERMANENTLY; 20 | import static play.test.Helpers.*; 21 | 22 | public class RedirectActionTest extends PlayTest { 23 | 24 | @Test 25 | public void noLangIsDefined_contentShouldBeInEnglish() throws Exception { 26 | List pages = Lists.newArrayList("/", "/report"); 27 | 28 | for (String test : pages) { 29 | Result result = route(request("forward.cat", test + "?param1=a")); 30 | assertThat(status(result), is(OK)); 31 | assertThat(contentAsString(result), containsHtmlLang("en")); 32 | } 33 | } 34 | 35 | @Test 36 | public void enInPath_contentShouldBeInEnglish() throws Exception { 37 | List pages = Lists.newArrayList("/en/", "/en/report"); 38 | 39 | for (String test : pages) { 40 | Result result = route(request("forward.cat", test + "?param1=a")); 41 | assertThat(status(result), is(NOT_FOUND)); 42 | } 43 | } 44 | 45 | @Test 46 | public void langInPath_contentShouldBeInTheGivenLang() throws Exception { 47 | Map tests = ImmutableMap.builder(). 48 | put("/es/", "es"). 49 | put("/ca/", "ca"). 50 | put("/es/report", "es"). 51 | put("/ca/report", "ca"). 52 | build(); 53 | 54 | for (Entry entry : tests.entrySet()) { 55 | String path = entry.getKey() + "?param1=a"; 56 | Result result = route(request("forward.cat", path)); 57 | 58 | assertThat(status(result), is(OK)); 59 | assertThat(contentAsString(result), containsHtmlLang(entry.getValue())); 60 | } 61 | } 62 | 63 | @Test 64 | public void incorrectPathStartsWithLang_shouldNotMatchAnyController() throws Exception { 65 | Result result = route(request("forward.cat", "/calculator?param1=a")); 66 | assertThat(result, is(nullValue())); 67 | } 68 | 69 | @Test 70 | public void wwwInSubdomain_requestShouldBeRedirected() throws Exception { 71 | Result result = route(request("www.forward.cat", "/?param1=a¶m2=b")); 72 | assertThat(status(result), is(MOVED_PERMANENTLY)); 73 | assertThat(location(result), is("https://forward.cat/?param1=a¶m2=b")); 74 | } 75 | 76 | private FakeRequest request(String host, String path) { 77 | return fakeRequest(GET, path) 78 | .withHeader(Http.HeaderNames.HOST, host); 79 | } 80 | 81 | private String location(Result result) { 82 | return result.toScala().header().headers().apply(LOCATION); 83 | } 84 | 85 | @Override 86 | public AbstractModule getModule() throws IOException { 87 | return new AbstractModule() { 88 | @Override 89 | protected void configure() { 90 | // Nothing to do 91 | } 92 | }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/DomainManager.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import play.i18n.Lang; 5 | import play.libs.F; 6 | import play.mvc.Action; 7 | import play.mvc.Http; 8 | import play.mvc.Result; 9 | 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | import static com.google.common.base.Preconditions.checkArgument; 16 | import static play.mvc.Results.movedPermanently; 17 | import static play.mvc.Results.notFound; 18 | 19 | public class DomainManager { 20 | 21 | private static final Lang DEFAULT_LANG = Lang.forCode("en"); 22 | private static final Pattern PATH_LANG_PATTTERN = Pattern.compile("^/(?[a-z]{2})([/\\?].*)?"); 23 | private static final Map SUBDOMAIN_REDIRECTS = ImmutableMap.builder() 24 | .put("www", "https://forward.cat") 25 | .build(); 26 | 27 | public static F.Promise redirect(Http.Context ctx, Action delegate) throws Throwable { 28 | Http.Request request = ctx.request(); 29 | String host = request.host(); 30 | 31 | // www subdomain: redirect 32 | if (host != null && host.startsWith("www.")) { 33 | String redirectUrl = buildUrl("www", request.path(), request.queryString()); 34 | return promise(movedPermanently(redirectUrl)); 35 | } 36 | 37 | // Lang in path: validate and set 38 | Matcher matcher = PATH_LANG_PATTTERN.matcher(request.path()); 39 | if (matcher.matches()) { 40 | String lang = matcher.group("lang"); 41 | if (!isValidLang(lang) || DEFAULT_LANG.code().equals(lang)) { 42 | return promise(notFound()); 43 | } else { 44 | ctx.changeLang(lang); 45 | return delegate.call(ctx); 46 | } 47 | } 48 | 49 | // No lang: use default 50 | ctx.changeLang(DEFAULT_LANG); 51 | return delegate.call(ctx); 52 | } 53 | 54 | public static String buildUrl(play.api.i18n.Lang lang, String path) { 55 | checkArgument(path.startsWith("/"), "path must start with /"); 56 | checkArgument(isValidLang(lang.language()), "%s is not a valid language", lang.language()); 57 | 58 | String language = lang.language(); 59 | if (language.equals(DEFAULT_LANG.language())) { 60 | return path; 61 | } else { 62 | return String.format("/%s%s", language, path); 63 | } 64 | } 65 | 66 | private static String buildUrl(String subdomain, String path, Map query) { 67 | String hostAndPath = SUBDOMAIN_REDIRECTS.get(subdomain); 68 | return hostAndPath + path + queryString(query); 69 | } 70 | 71 | private static String queryString(Map query) { 72 | Optional params = query.entrySet().stream() 73 | .filter(e -> e.getKey() != null && e.getValue() != null && e.getValue().length > 0) 74 | .map(e -> e.getKey() + "=" + e.getValue()[0]) 75 | .reduce((p1, p2) -> p1 + "&" + p2); 76 | return params.map(p -> "?" + p).orElse(""); 77 | } 78 | 79 | private static boolean isValidLang(String langCode) { 80 | Lang lang = Lang.forCode(langCode); 81 | return Lang.availables().contains(lang); 82 | } 83 | 84 | private static F.Promise promise(A result) { 85 | return F.Promise.pure(result); 86 | } 87 | 88 | private DomainManager() { 89 | // Non-instantiable 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/ControllerUtils.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.common.base.Charsets; 5 | import com.google.common.hash.HashFunction; 6 | import com.google.common.hash.Hasher; 7 | import com.google.common.hash.Hashing; 8 | import org.apache.mailet.MailAddress; 9 | import play.i18n.Lang; 10 | import play.mvc.Http; 11 | 12 | import javax.mail.internet.AddressException; 13 | import java.util.Optional; 14 | import java.util.regex.Pattern; 15 | 16 | public class ControllerUtils { 17 | private static final Pattern HOST_LANG_PATTTERN = Pattern.compile("^[a-z]{2}\\..*"); 18 | private static final String DOMAIN_NAME = "forward.cat"; 19 | 20 | public static Lang getBestLanguage(Http.Request request, Lang preferred) { 21 | // Getting the info from the hostname 22 | String host = request.host(); 23 | if (host != null && HOST_LANG_PATTTERN.matcher(host).matches()) { 24 | Lang lang = Lang.forCode(host.substring(0, 2)); 25 | if (Lang.availables().contains(lang)) { 26 | return lang; 27 | } 28 | } 29 | 30 | // Returning the preferred language 31 | return preferred; 32 | } 33 | 34 | private static HashFunction HASH_FUNCTION = Hashing.murmur3_32(0); // Setting seed for consistent hashing 35 | 36 | /** 37 | * Given a {@link com.forwardcat.common.ProxyMail}, returns a hash that identifies it uniquely 38 | */ 39 | public static String getHash(ProxyMail proxy) { 40 | Hasher hasher = HASH_FUNCTION.newHasher(); 41 | hasher.putString(proxy.getCreationTime().toString(), Charsets.UTF_8); 42 | return hasher.hash().toString(); 43 | } 44 | 45 | /** 46 | * Given a {@link com.forwardcat.common.ProxyMail} and a hash, returns true if the hash is valid 47 | */ 48 | public static boolean isAuthenticated(Optional proxy, String hash) { 49 | return proxy.isPresent() && hash.equals(getHash(proxy.get())); 50 | } 51 | 52 | /** 53 | * Given a username and a domain name, returns a {@link org.apache.mailet.MailAddress} 54 | */ 55 | public static Optional getMailAddress(String username) { 56 | return getMailAddress(username, DOMAIN_NAME); 57 | } 58 | 59 | /** 60 | * Given a username and a domain name, returns a {@link org.apache.mailet.MailAddress} 61 | */ 62 | public static Optional getMailAddress(String username, String domainName) { 63 | if (username != null && domainName != null) { 64 | try { 65 | return Optional.of(new MailAddress(username.toLowerCase(), domainName.toLowerCase())); 66 | } catch (AddressException e) { 67 | // Do nothing 68 | } 69 | } 70 | return Optional.empty(); 71 | } 72 | 73 | /** 74 | * Returns true if the given {@link MailAddress} has the local domain name 75 | */ 76 | public static boolean isLocal(MailAddress userMail) { 77 | return DOMAIN_NAME.equals(userMail.getDomain()); 78 | } 79 | 80 | /** 81 | * Converts the given String to a {@link MailAddress} 82 | */ 83 | public static Optional toMailAddress(String emailAddress) { 84 | if (emailAddress != null) { 85 | try { 86 | return Optional.of(new MailAddress(emailAddress.toLowerCase())); 87 | } catch (AddressException ex) { 88 | // Do nothing 89 | } 90 | } 91 | return Optional.empty(); 92 | } 93 | 94 | private ControllerUtils() { 95 | // Non-instantiable 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/Repository.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.avaje.ebean.Ebean; 4 | import com.avaje.ebean.RawSql; 5 | import com.avaje.ebean.RawSqlBuilder; 6 | import com.avaje.ebean.bean.EntityBean; 7 | import com.forwardcat.common.ProxyMail; 8 | import com.forwardcat.common.User; 9 | import org.apache.mailet.MailAddress; 10 | 11 | import javax.annotation.Nonnull; 12 | import java.util.Date; 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.Set; 16 | 17 | import static models.ExpirationUtils.now; 18 | import static models.ExpirationUtils.toDate; 19 | 20 | public class Repository { 21 | 22 | public void save(EntityBean entity) { 23 | Ebean.save(entity); 24 | } 25 | 26 | public void delete(ProxyMail proxy) { 27 | Ebean.delete(proxy); 28 | } 29 | 30 | /** 31 | * Returns the {@link ProxyMail} linked to the given mail address 32 | */ 33 | public Optional getProxy(@Nonnull MailAddress address) { 34 | return Optional.ofNullable(Ebean.find(ProxyMail.class, toString(address))); 35 | } 36 | 37 | public Optional getUser(@Nonnull MailAddress address) { 38 | return Optional.ofNullable( 39 | Ebean.find(User.class) 40 | .where() 41 | .eq("email_address", toString(address)) 42 | .findUnique() 43 | ); 44 | } 45 | 46 | public boolean proxyExists(@Nonnull MailAddress proxyMailAddress) { 47 | int count = Ebean.createQuery(ProxyMail.class) 48 | .where() 49 | .eq("proxy_address", toString(proxyMailAddress)) 50 | .findRowCount(); 51 | return count >= 1; 52 | } 53 | 54 | public void update(@Nonnull ProxyMail proxy) { 55 | Ebean.update(proxy); 56 | } 57 | 58 | public Set getExpiringProxies() { 59 | return Ebean.find(ProxyMail.class) 60 | .where() 61 | .lt("expiration_time", toDate(now().plusDays(1))) 62 | .eq("expiration_notified", false) 63 | .findSet(); 64 | } 65 | 66 | public void removeExpiredProxies() { 67 | List expiredProxies = Ebean.find(ProxyMail.class) 68 | .where() 69 | .lt("expiration_time", new Date()) 70 | .findIds(); 71 | 72 | delete(ProxyMail.class, expiredProxies); 73 | } 74 | 75 | public void removeNonActivatedProxies() { 76 | List ids = Ebean.find(ProxyMail.class) 77 | .where() 78 | .eq("active", false) 79 | .lt("creation_time", toDate(now().plusMinutes(-30))) 80 | .findIds(); 81 | 82 | delete(ProxyMail.class, ids); 83 | } 84 | 85 | public void removeUsersWithoutProxies() { 86 | String sql = "select u.id from users u where u.id not in " + 87 | "(select user_id from proxy_mail)"; 88 | 89 | RawSql rawSql = RawSqlBuilder 90 | .parse(sql) 91 | .columnMapping("u.id", "id") 92 | .create(); 93 | 94 | List ids = Ebean.find(User.class) 95 | .setRawSql(rawSql) 96 | .findIds(); 97 | 98 | delete(User.class, ids); 99 | } 100 | 101 | private void delete(Class clazz, List ids) { 102 | if (!ids.isEmpty()) { 103 | Ebean.delete(clazz, ids); 104 | } 105 | } 106 | 107 | private String toString(@Nonnull MailAddress address) { 108 | return address.toString().toLowerCase(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/java/com/forwardcat/james/FileDKIMSign.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.james; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.apache.commons.ssl.PKCS8Key; 5 | import org.apache.james.jdkim.mailets.DKIMSign; 6 | import org.springframework.context.ResourceLoaderAware; 7 | import org.springframework.core.io.Resource; 8 | import org.springframework.core.io.ResourceLoader; 9 | 10 | import javax.mail.MessagingException; 11 | import java.io.ByteArrayInputStream; 12 | import java.io.IOException; 13 | import java.security.GeneralSecurityException; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.security.PrivateKey; 16 | import java.security.spec.InvalidKeySpecException; 17 | 18 | /** 19 | * Mailet based on {@link DKIMSign} that loads the private key from a file 20 | * instead of from a init parameter. 21 | *

22 | * Since the field privateKey is private it {@link DKIMSign}, a new field privateKey 23 | * is declared in order to override {@link #getPrivateKey()}. 24 | *

25 | * The {@link #init()} method of the superclass is called first in order to load all 26 | * the other init parameters. 27 | */ 28 | public class FileDKIMSign extends DKIMSign implements ResourceLoaderAware { 29 | 30 | static final String PRIVATE_KEY_FILE_PARAM = "privateKeyFile"; 31 | 32 | private PrivateKey privateKey; 33 | private ResourceLoader resourceLoader; 34 | 35 | @Override 36 | protected PrivateKey getPrivateKey() { 37 | return privateKey; 38 | } 39 | 40 | @Override 41 | public void setResourceLoader(ResourceLoader resourceLoader) { 42 | this.resourceLoader = resourceLoader; 43 | } 44 | 45 | @Override 46 | public void init() throws MessagingException { 47 | // Loading all the parameters: privateKey will fail 48 | try { 49 | super.init(); 50 | } catch (Exception ex) { 51 | // Do nothing! 52 | } 53 | 54 | byte[] privateKey = loadPrivateKey(); 55 | String privateKeyPassword = getInitParameter("privateKeyPassword", null); 56 | try { 57 | PKCS8Key pkcs8 = new PKCS8Key(new ByteArrayInputStream( 58 | privateKey), 59 | privateKeyPassword != null ? privateKeyPassword 60 | .toCharArray() : null); 61 | this.privateKey = pkcs8.getPrivateKey(); 62 | // privateKey = DKIMSigner.getPrivateKey(privateKeyString); 63 | } catch (NoSuchAlgorithmException e) { 64 | throw new MessagingException("Unknown private key algorithm: " 65 | + e.getMessage(), e); 66 | } catch (InvalidKeySpecException e) { 67 | throw new MessagingException( 68 | "PrivateKey should be in base64 encoded PKCS8 (der) format: " 69 | + e.getMessage(), e); 70 | } catch (GeneralSecurityException e) { 71 | throw new MessagingException("General security exception: " 72 | + e.getMessage(), e); 73 | } 74 | } 75 | 76 | private byte[] loadPrivateKey() throws MessagingException { 77 | String privateKeyFile = getInitParameter(PRIVATE_KEY_FILE_PARAM); 78 | if (privateKeyFile == null) { 79 | throw new MessagingException("The init parameter 'privateKeyFile' has not been set"); 80 | } 81 | 82 | Resource resource = resourceLoader.getResource(privateKeyFile); 83 | if (!resource.exists()) { 84 | throw new MessagingException("Resource " + privateKeyFile + " has not been found. Make sure " + 85 | "is in the conf/ directory"); 86 | } 87 | 88 | try { 89 | return IOUtils.toByteArray(resource.getInputStream()); 90 | } catch (IOException e) { 91 | throw new MessagingException("Error while reading private key", e); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/ReportTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.google.inject.AbstractModule; 5 | import models.MailSender; 6 | import models.Repository; 7 | import org.apache.mailet.MailAddress; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | import play.mvc.Result; 13 | import play.test.FakeRequest; 14 | 15 | import java.io.IOException; 16 | import java.util.Optional; 17 | 18 | import static controllers.TestUtils.*; 19 | import static org.hamcrest.Matchers.is; 20 | import static org.junit.Assert.assertThat; 21 | import static org.mockito.Matchers.anyString; 22 | import static org.mockito.Mockito.*; 23 | import static play.test.Helpers.*; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class ReportTest extends PlayTest { 27 | 28 | MailAddress inactiveProxy = toMailAddress("inactive@forward.cat"); 29 | ProxyMail inactive = inactiveProxy(inactiveProxy); 30 | 31 | MailAddress blockedProxy = toMailAddress("blocked@forward.cat"); 32 | ProxyMail blocked = activeProxy(blockedProxy, true); 33 | 34 | MailAddress spammerProxy = toMailAddress("spammer@forward.cat"); 35 | ProxyMail spammer = activeProxy(blockedProxy, false); 36 | 37 | @Mock Repository repository; 38 | @Mock MailSender mailSender; 39 | 40 | @Override 41 | public AbstractModule getModule() throws IOException { 42 | when(repository.getProxy(any(MailAddress.class))).thenAnswer(invocationOnMock -> { 43 | String strAddress = invocationOnMock.getArguments()[0].toString(); 44 | if (inactive.toString().equals(strAddress)) { 45 | return Optional.of(inactive); 46 | } else if (blocked.toString().equals(strAddress)) { 47 | return Optional.of(blocked); 48 | } else if (spammerProxy.toString().equals(strAddress)) { 49 | return Optional.of(spammer); 50 | } 51 | return Optional.empty(); 52 | }); 53 | 54 | return new AbstractModule() { 55 | @Override 56 | protected void configure() { 57 | bind(Repository.class).toInstance(repository); 58 | bind(MailSender.class).toInstance(mailSender); 59 | } 60 | }; 61 | } 62 | 63 | @Test 64 | public void nonLocalEmail_dontSendEmail() throws Exception { 65 | Result route = route(request("user@mail.com")); 66 | verifyZeroInteractions(mailSender); 67 | assertThat(status(route), is(OK)); 68 | } 69 | 70 | @Test 71 | public void nonExistantProxy_dontSendEmail() throws Exception { 72 | Result route = route(request("not_there@forward.cat")); 73 | verifyZeroInteractions(mailSender); 74 | assertThat(status(route), is(OK)); 75 | } 76 | 77 | @Test 78 | public void nonActiveProxy_dontSendEmail() throws Exception { 79 | Result route = route(request(inactiveProxy.toString())); 80 | verifyZeroInteractions(mailSender); 81 | assertThat(status(route), is(OK)); 82 | } 83 | 84 | @Test 85 | public void blockedProxy_dontSendEmail() throws Exception { 86 | Result route = route(request(blockedProxy.toString())); 87 | verifyZeroInteractions(mailSender); 88 | assertThat(status(route), is(OK)); 89 | } 90 | 91 | @Test 92 | public void regularProxy_sendEmail() throws Exception { 93 | Result route = route(request(spammerProxy.toString())); 94 | assertThat(status(route), is(OK)); 95 | verify(mailSender).sendHtmlMail(any(MailAddress.class), anyString(), anyString()); 96 | } 97 | 98 | private FakeRequest request(String proxy) { 99 | return fakeRequest(GET, "/report-user?proxy=" + proxy + "&message=hey%20there"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/main/java/com/forwardcat/james/ForwardCatResourcesProvider.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.james; 2 | 3 | import com.avaje.ebean.EbeanServer; 4 | import com.avaje.ebean.EbeanServerFactory; 5 | import com.avaje.ebean.config.DataSourceConfig; 6 | import com.avaje.ebean.config.ServerConfig; 7 | import com.forwardcat.common.ProxyMail; 8 | import com.forwardcat.common.User; 9 | import redis.clients.jedis.JedisPool; 10 | import redis.clients.jedis.JedisPoolConfig; 11 | 12 | import javax.annotation.PostConstruct; 13 | import javax.annotation.PreDestroy; 14 | import java.io.IOException; 15 | import java.util.Properties; 16 | 17 | /** 18 | * Utility class that initializes and holds the resources used by the Forward Cat 19 | * mailets 20 | */ 21 | public class ForwardCatResourcesProvider { 22 | 23 | private static final String PROPS_FILE = "forward-cat.properties"; 24 | 25 | private JedisPool pool; 26 | private EbeanServer ebeanServer; 27 | 28 | public JedisPool getPool() { 29 | return pool; 30 | } 31 | 32 | public EbeanServer getEbeanServer() { 33 | return ebeanServer; 34 | } 35 | 36 | @PostConstruct 37 | public void init() { 38 | // Loading properties from file 39 | Properties props = new Properties(); 40 | try { 41 | ClassLoader loader = Thread.currentThread().getContextClassLoader(); 42 | props.load(loader.getResourceAsStream(PROPS_FILE)); 43 | } catch (IOException e) { 44 | // We can't do anything about it 45 | throw new IllegalStateException("Is " + PROPS_FILE + " in the classpath?", e); 46 | } 47 | 48 | configEbean(props); 49 | configRedis(props); 50 | } 51 | 52 | private void configRedis(Properties props) { 53 | // Configuring Jedis 54 | JedisPoolConfig config = new JedisPoolConfig(); 55 | config.setMaxActive(Integer.parseInt(props.getProperty("max-active-connections", "10"))); 56 | config.setMinIdle(Integer.parseInt(props.getProperty("min-idle-connections", "1"))); 57 | 58 | String redisHost = props.getProperty("redis-host", "localhost"); 59 | pool = new JedisPool(config, redisHost); 60 | } 61 | 62 | private void configEbean(Properties props) { 63 | // http://www.avaje.org/ebean/getstarted_programmatic.html 64 | ServerConfig config = new ServerConfig(); 65 | config.setName("fwdconnection"); 66 | 67 | // Define DataSource parameters 68 | DataSourceConfig postgresDb = new DataSourceConfig(); 69 | postgresDb.setDriver(props.getProperty("datasource.pg.databaseDriver")); 70 | postgresDb.setUsername(props.getProperty("datasource.pg.username")); 71 | postgresDb.setPassword(props.getProperty("datasource.pg.password")); 72 | postgresDb.setUrl(props.getProperty("datasource.pg.databaseUrl")); 73 | postgresDb.setHeartbeatSql(props.getProperty("datasource.pg.heartbeatsql")); 74 | 75 | config.setDataSourceConfig(postgresDb); 76 | 77 | // set DDL options... 78 | config.setDdlGenerate(false); 79 | config.setDdlRun(false); 80 | 81 | // config.setDefaultServer(false); 82 | // config.setRegister(false); 83 | 84 | // automatically determine the DatabasePlatform 85 | // using the jdbc driver 86 | // config.setDatabasePlatform(new PostgresPlatform()); 87 | 88 | config.addClass(ProxyMail.class); 89 | config.addClass(User.class); 90 | 91 | // specify jars to search for entity beans 92 | // config.addJar("someJarThatContainsEntityBeans.jar"); 93 | 94 | // create the EbeanServer instance 95 | ebeanServer = EbeanServerFactory.create(config); 96 | } 97 | 98 | @PreDestroy 99 | public void shutdown() { 100 | pool.destroy(); 101 | ebeanServer.shutdown(true, true); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-common/src/main/java/com/forwardcat/common/ProxyMail.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.common; 2 | 3 | import org.apache.mailet.MailAddress; 4 | 5 | import javax.persistence.*; 6 | import java.time.Instant; 7 | import java.util.Date; 8 | 9 | import static com.google.common.base.Preconditions.checkState; 10 | import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; 11 | 12 | /** 13 | * Temporary mail 14 | */ 15 | @Entity(name = "proxies") 16 | public class ProxyMail { 17 | 18 | @Id @Column(nullable = false) 19 | private String proxyAddress; 20 | @ManyToOne 21 | private User user; 22 | @Column(nullable = false) @Temporal(TemporalType.TIMESTAMP) 23 | private Date creationTime; 24 | @Column(nullable = false) 25 | private String lang; 26 | @Column(nullable = false) @Temporal(TemporalType.TIMESTAMP) 27 | private Date expirationTime; 28 | private boolean blocked = false; 29 | private boolean active = false; 30 | private boolean expirationNotified = false; 31 | 32 | public String getProxyAddress() { 33 | return proxyAddress; 34 | } 35 | 36 | public void setProxyAddress(String proxyAddress) { 37 | this.proxyAddress = proxyAddress; 38 | } 39 | 40 | public User getUser() { 41 | return user; 42 | } 43 | 44 | public void setUser(User user) { 45 | this.user = user; 46 | } 47 | 48 | public Date getCreationTime() { 49 | return creationTime; 50 | } 51 | 52 | public void setCreationTime(Date creationTime) { 53 | this.creationTime = creationTime; 54 | } 55 | 56 | public Date getExpirationTime() { 57 | return expirationTime; 58 | } 59 | 60 | public void setExpirationTimeStr(String expirationTime) { 61 | this.expirationTime = Date.from(Instant.from(ISO_OFFSET_DATE_TIME.parse(expirationTime))); 62 | } 63 | 64 | public void setExpirationTime(Date expirationTime) { 65 | this.expirationTime = expirationTime; 66 | } 67 | 68 | public String getLang() { 69 | return lang; 70 | } 71 | 72 | public void setLang(String lang) { 73 | this.lang = lang; 74 | } 75 | 76 | public void block() { 77 | checkState(!blocked, "Proxy is already blocked"); 78 | blocked = true; 79 | } 80 | 81 | public void setBlocked(boolean blocked) { 82 | this.blocked = blocked; 83 | } 84 | 85 | public boolean isBlocked() { 86 | return blocked; 87 | } 88 | 89 | public void activate() { 90 | checkState(!active, "Proxy is already active"); 91 | active = true; 92 | } 93 | 94 | public boolean isActive() { 95 | return active; 96 | } 97 | 98 | public void setActive(boolean active) { 99 | this.active = active; 100 | } 101 | 102 | public boolean isExpirationNotified() { 103 | return expirationNotified; 104 | } 105 | 106 | public void setExpirationNotified(boolean expirationNotified) { 107 | this.expirationNotified = expirationNotified; 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return "ProxyMail{" + 113 | "proxyAddress='" + proxyAddress + '\'' + 114 | ", creationTime='" + creationTime + '\'' + 115 | ", lang='" + lang + '\'' + 116 | ", expirationTime='" + expirationTime + '\'' + 117 | ", blocked=" + blocked + 118 | ", active=" + active + 119 | ", expirationNotified=" + expirationNotified + 120 | '}'; 121 | } 122 | 123 | public static ProxyMail create(MailAddress proxyAddress, MailAddress userAddress, Date creationTime, Date expirationTime, String lang) { 124 | ProxyMail proxy = new ProxyMail(); 125 | proxy.setProxyAddress(proxyAddress.toString().toLowerCase()); 126 | proxy.setCreationTime(creationTime); 127 | proxy.setExpirationTime(expirationTime); 128 | proxy.setLang(lang); 129 | return proxy; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/test/java/com/forwardcat/james/FileDKIMSignTest.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.james; 2 | 3 | import org.apache.mailet.MailetConfig; 4 | import org.apache.mailet.MailetContext; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | import org.springframework.core.io.Resource; 11 | import org.springframework.core.io.ResourceLoader; 12 | 13 | import javax.mail.MessagingException; 14 | import java.io.ByteArrayInputStream; 15 | import java.io.IOException; 16 | import java.net.UnknownHostException; 17 | 18 | import static org.mockito.Mockito.when; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class FileDKIMSignTest { 22 | 23 | FileDKIMSign mailet; 24 | 25 | String privateKeyFile = "privateKey.pem"; 26 | 27 | @Mock ResourceLoader resourcesLoader; 28 | @Mock Resource resource; 29 | @Mock MailetConfig config; 30 | @Mock MailetContext context; 31 | 32 | @Before 33 | public void setUp() throws MessagingException, UnknownHostException { 34 | mailet = new FileDKIMSign(); 35 | mailet.setResourceLoader(resourcesLoader); 36 | when(resourcesLoader.getResource(privateKeyFile)).thenReturn(resource); 37 | 38 | when(config.getMailetContext()).thenReturn(context); 39 | } 40 | 41 | @Test(expected = MessagingException.class) 42 | public void fileParamNotSet_shouldThrowException() throws MessagingException { 43 | mailet.init(config); 44 | } 45 | 46 | @Test(expected = MessagingException.class) 47 | public void resourceDoesNotExist_shouldThrowException() throws MessagingException { 48 | when(config.getInitParameter(FileDKIMSign.PRIVATE_KEY_FILE_PARAM)).thenReturn(privateKeyFile); 49 | when(resource.exists()).thenReturn(false); 50 | 51 | mailet.init(config); 52 | } 53 | 54 | @Test(expected = MessagingException.class) 55 | public void privateKeyNotValid_shouldThrowException() throws MessagingException, IOException { 56 | when(config.getInitParameter(FileDKIMSign.PRIVATE_KEY_FILE_PARAM)).thenReturn(privateKeyFile); 57 | when(resource.exists()).thenReturn(true); 58 | when(resource.getInputStream()).thenReturn(new ByteArrayInputStream("not a valid private key".getBytes())); 59 | 60 | mailet.init(config); 61 | } 62 | 63 | @Test 64 | public void privateKeyValid_everythingEndsFine() throws MessagingException, IOException { 65 | when(config.getInitParameter(FileDKIMSign.PRIVATE_KEY_FILE_PARAM)).thenReturn(privateKeyFile); 66 | when(resource.exists()).thenReturn(true); 67 | when(resource.getInputStream()).thenReturn(new ByteArrayInputStream(getPrivateKeyByteArray())); 68 | 69 | mailet.init(config); 70 | } 71 | 72 | private byte[] getPrivateKeyByteArray() { 73 | return ("-----BEGIN RSA PRIVATE KEY-----\n" + 74 | "MIICXQIBAAKBgQDMiitd8SM3uN+c8a/73ESdTrcISLAF306hHvQxglBC4/WPIdvr\n" + 75 | "fTURDPeSVPNZ6bjP6sVPy/VsaFfAwF1b6ZxF7PEXlK8gYp6E8lkGSWI7y722aGzJ\n" + 76 | "5CIu6DM8NtHmXlAs45vEX4poX0pfeMVqNakyxa0pm8tAFe0ewzW8pblEtQIDAQAB\n" + 77 | "AoGADO4jJbIrxscCI9rHhEV9dPBX88cckZJ3Vwos58BUMJZWnLDIRU/J/gTy1aZX\n" + 78 | "J/T1gPdXd97t6eeCvKWsgTX4cfoxqgfYdTW2MoZuQWDpS6xhKjDEHI0Q4M0wta34\n" + 79 | "j3pLVRlRk3+TCLVOjE1ZjJ1Hd+Syn8HodRBvf5x13hVeZPUCQQD2Ac/eUIATd2BL\n" + 80 | "biayEmVvZF87ps3ej67+JY4d/aQuce60dhtCCpBwfwFqTrDGOWMOo539nEUK1B8D\n" + 81 | "etyOe3jHAkEA1NklI8VxZjzRMgXDprfLoRIPcJnobE3bq2120FVfu2dlAq19DIko\n" + 82 | "Dfume1lAlS+Zk2npQpiVR73avRrtPNeyowJAQlT0vqYIEreigFRAHM23ChUPVJ9C\n" + 83 | "bVtivOZVbqLAjUFtMr2R1fnRPnQQZqC3K4u3uO/HHuXu+998SUzsgYKrawJBAJfs\n" + 84 | "bj/8HBb3bfIgfygupB/RvkeG84jqgdL4jQfjCDPBdy3UGx+pfneMmaYNbLWPhjTc\n" + 85 | "Meyg8FyGvOyhnZgB9bUCQQC45QKz2CYbtJKxMGHzuzaIgyPrSsqMDHR8Gj0uifyi\n" + 86 | "o8PZOBroz7zUJr11oQb80PuGPKYeSKlCeCGmpPZ0ffFG\n" + 87 | "-----END RSA PRIVATE KEY-----").getBytes(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /forward-cat-website/app/models/StatsRepository.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import com.avaje.ebean.Ebean; 4 | import com.avaje.ebean.FutureRowCount; 5 | import com.forwardcat.common.ProxyMail; 6 | import com.forwardcat.common.User; 7 | import com.google.common.base.Strings; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import redis.clients.jedis.Jedis; 11 | import redis.clients.jedis.JedisPool; 12 | import redis.clients.jedis.Pipeline; 13 | import redis.clients.jedis.Response; 14 | 15 | import javax.inject.Inject; 16 | 17 | import static com.forwardcat.common.RedisKeys.*; 18 | import static models.JedisHelper.returnJedisIfNotNull; 19 | import static models.JedisHelper.returnJedisOnException; 20 | 21 | public class StatsRepository { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(StatsRepository.class.getName()); 24 | private final JedisPool jedisPool; 25 | 26 | @Inject 27 | public StatsRepository(JedisPool jedisPool) { 28 | this.jedisPool = jedisPool; 29 | } 30 | 31 | public void incrementCounter(String counter) { 32 | Jedis jedis = null; 33 | try { 34 | jedis = jedisPool.getResource(); 35 | jedis.incr(counter); 36 | } catch (Exception ex) { 37 | LOGGER.error("Error while connecting to Redis", ex); 38 | returnJedisOnException(jedisPool, jedis, ex); 39 | jedis = null; 40 | } finally { 41 | returnJedisIfNotNull(jedisPool, jedis); 42 | } 43 | } 44 | 45 | public StatsCounters getStats(){ 46 | Jedis jedis = null; 47 | StatsCounters counters = null; 48 | try { 49 | jedis = jedisPool.getResource(); 50 | 51 | Pipeline pipeline = jedis.pipelined(); 52 | 53 | FutureRowCount activeUsersRs = Ebean.find(User.class).findFutureRowCount(); 54 | FutureRowCount activeProxiesRs = Ebean.find(ProxyMail.class).findFutureRowCount(); 55 | Response emailsBlockedRs = pipeline.get(EMAILS_BLOCKED_COUNTER); 56 | Response emailsForwardedRs = pipeline.get(EMAILS_FORWARDED_COUNTER); 57 | Response proxiesActivatedRs = pipeline.get(PROXIES_ACTIVATED_COUNTER); 58 | Response spammerProxiesBlockedRs = pipeline.get(SPAMMER_PROXIES_BLOCKED_COUNTER); 59 | 60 | pipeline.sync(); 61 | 62 | counters = new StatsCounters( 63 | activeUsersRs.get(), 64 | activeProxiesRs.get(), 65 | toInt(emailsBlockedRs.get()), 66 | toInt(emailsForwardedRs.get()), 67 | toInt(proxiesActivatedRs.get()), 68 | toInt(spammerProxiesBlockedRs.get())); 69 | 70 | } catch (Exception ex) { 71 | LOGGER.error("Error while connecting to Redis", ex); 72 | returnJedisOnException(jedisPool, jedis, ex); 73 | jedis = null; 74 | } finally { 75 | returnJedisIfNotNull(jedisPool, jedis); 76 | } 77 | return counters; 78 | } 79 | 80 | private int toInt(String str) { 81 | if (Strings.isNullOrEmpty(str)) { 82 | return 0; 83 | } 84 | return Integer.parseInt(str); 85 | } 86 | 87 | public static class StatsCounters { 88 | public final int activeUsers; 89 | public final int activeProxies; 90 | public final int emailsBlocked; 91 | public final int emailsForwarded; 92 | public final int proxiesActivated; 93 | public final int spammerProxiesBlocked; 94 | 95 | public StatsCounters(int activeUsers, int activeProxies, int emailsBlocked, int emailsForwarded, int 96 | proxiesActivated, int spammerProxiesBlocked) { 97 | this.activeUsers = activeUsers; 98 | this.activeProxies = activeProxies; 99 | this.emailsBlocked = emailsBlocked; 100 | this.emailsForwarded = emailsForwarded; 101 | this.proxiesActivated = proxiesActivated; 102 | this.spammerProxiesBlocked = spammerProxiesBlocked; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /forward-cat-website/test/controllers/AddProxyTest.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import com.forwardcat.common.ProxyMail; 4 | import com.forwardcat.common.User; 5 | import com.google.inject.AbstractModule; 6 | import models.MailSender; 7 | import models.Repository; 8 | import org.apache.mailet.MailAddress; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.runners.MockitoJUnitRunner; 13 | import play.mvc.Result; 14 | import play.test.FakeRequest; 15 | 16 | import javax.mail.internet.AddressException; 17 | import java.io.IOException; 18 | import java.util.Date; 19 | import java.util.Optional; 20 | 21 | import static controllers.TestUtils.toMailAddress; 22 | import static controllers.TestUtils.whenAddressExistsReturn; 23 | import static java.util.Optional.empty; 24 | import static org.hamcrest.Matchers.is; 25 | import static org.junit.Assert.assertThat; 26 | import static org.mockito.Matchers.anyString; 27 | import static org.mockito.Mockito.*; 28 | import static play.test.Helpers.*; 29 | 30 | @RunWith(MockitoJUnitRunner.class) 31 | public class AddProxyTest extends PlayTest { 32 | 33 | MailAddress proxyMail = toMailAddress("test@forward.cat"); 34 | 35 | @Mock MailSender mailSender; 36 | @Mock Repository repository; 37 | 38 | @Override 39 | public AbstractModule getModule() throws IOException, AddressException { 40 | return new AbstractModule() { 41 | @Override 42 | protected void configure() { 43 | bind(Repository.class).toInstance(repository); 44 | bind(MailSender.class).toInstance(mailSender); 45 | } 46 | }; 47 | } 48 | 49 | @Test 50 | public void wrongEmail_sendBadRequest() throws Exception { 51 | Result route = route(request("test", "not an address")); 52 | assertThat(status(route), is(BAD_REQUEST)); 53 | } 54 | 55 | @Test 56 | public void wrongUsername_sendBadRequest() throws Exception { 57 | Result route = route(request("not valid", "user@mail.com")); 58 | assertThat(status(route), is(BAD_REQUEST)); 59 | } 60 | 61 | @Test 62 | public void chainedProxy_sendBadRequest() throws Exception { 63 | Result route = route(request("test", "email@forward.cat")); 64 | assertThat(status(route), is(BAD_REQUEST)); 65 | } 66 | 67 | @Test 68 | public void proxyAlreadyExists_sendBadRequest() throws Exception { 69 | whenAddressExistsReturn(repository, proxyMail, true); 70 | 71 | Result route = route(request("test", "user@mail.com")); 72 | assertThat(status(route), is(BAD_REQUEST)); 73 | } 74 | 75 | @Test(expected = RuntimeException.class) 76 | public void connectionError_sendBadRequest() throws Exception { 77 | when(repository.proxyExists(proxyMail)).thenThrow(new RuntimeException()); 78 | 79 | Result route = route(request("test", "user@mail.com")); 80 | assertThat(status(route), is(BAD_REQUEST)); 81 | } 82 | 83 | @Test 84 | public void tooManyProxies_sendBadRequest() throws Exception { 85 | ProxyMail[] proxies = {mock(ProxyMail.class), mock(ProxyMail.class), mock(ProxyMail.class)}; 86 | User user = User.create(toMailAddress("user@mail.com"), new Date(), proxies); 87 | whenAddressExistsReturn(repository, proxyMail, false); 88 | when(repository.getUser(toMailAddress("user@mail.com"))).thenReturn(Optional.of(user)); 89 | 90 | Result route = route(request("test", "user@mail.com")); 91 | assertThat(status(route), is(BAD_REQUEST)); 92 | } 93 | 94 | @Test 95 | public void everythingFine_sendMailAndGoodResponse() throws Exception { 96 | whenAddressExistsReturn(repository, proxyMail, false); 97 | when(repository.getUser(toMailAddress("user@mail.com"))).thenReturn(empty()); 98 | 99 | Result route = route(request("test", "user@mail.com")); 100 | assertThat(status(route), is(OK)); 101 | 102 | verify(repository).save(any(User.class)); 103 | verify(mailSender).sendHtmlMail(any(MailAddress.class), anyString(), anyString()); 104 | } 105 | 106 | private FakeRequest request(String proxy, String email) { 107 | return fakeRequest(GET, "/add?proxy=" + proxy + "&email=" + email); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.forwardcat 9 | parent 10 | 1.0-SNAPSHOT 11 | 12 | james 13 | 1.0-SNAPSHOT 14 | Forward Cat - James 15 | 16 | 17 | 3.0.0-beta5-SNAPSHOT 18 | 19 | 20 | 21 | 22 | com.forwardcat 23 | common 24 | 1.0-SNAPSHOT 25 | 26 | 27 | org.apache.james 28 | james-server-mailets 29 | ${james.server.version} 30 | provided 31 | 32 | 33 | org.apache.james 34 | james-server-core 35 | ${james.server.version} 36 | provided 37 | 38 | 39 | 40 | org.apache.james.jdkim 41 | apache-jdkim-library 42 | 0.2 43 | 44 | 45 | org.apache.james.jdkim 46 | apache-jdkim-mailets 47 | 0.2 48 | 49 | 50 | org.springframework 51 | spring-context 52 | 3.1.0.RELEASE 53 | provided 54 | 55 | 56 | 57 | org.hamcrest 58 | hamcrest-all 59 | test 60 | 1.3 61 | 62 | 63 | org.avaje.ebeanorm 64 | avaje-ebeanorm 65 | 3.3.1 66 | 67 | 68 | postgresql 69 | postgresql 70 | 9.1-901-1.jdbc4 71 | 72 | 73 | org.slf4j 74 | slf4j-api 75 | 1.7.12 76 | 77 | 78 | 80 | redis.clients 81 | jedis 82 | 2.1.0 83 | jar 84 | 85 | 86 | 88 | commons-configuration 89 | commons-configuration 90 | 1.9 91 | 92 | 93 | 94 | junit 95 | junit 96 | 4.11 97 | test 98 | 99 | 100 | 101 | org.mockito 102 | mockito-all 103 | 1.9.5 104 | test 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-shade-plugin 113 | 1.7.1 114 | 115 | 116 | package 117 | 118 | shade 119 | 120 | 121 | ${project.artifactId} 122 | false 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /forward-cat-website/test/models/TestHttpRequest.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import play.api.http.MediaRange; 4 | import play.i18n.Lang; 5 | import play.mvc.Http; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class TestHttpRequest extends Http.Request { 11 | 12 | private Http.RequestBody body; 13 | private String uri; 14 | private String method = "GET"; 15 | private String version = "HTTP/1.1"; 16 | private String remoteAddress = "127.0.0.1"; 17 | private boolean secure = false; 18 | private String host; 19 | private String path; 20 | private List acceptLanguages; 21 | private List accept; 22 | private List acceptedTypes; 23 | private boolean accepts; 24 | private Map queryString; 25 | private Http.Cookies cookies; 26 | private Map headers; 27 | 28 | @Override 29 | public Http.RequestBody body() { 30 | return body; 31 | } 32 | 33 | public TestHttpRequest setBody(Http.RequestBody body) { 34 | this.body = body; 35 | return this; 36 | } 37 | 38 | @Override 39 | public String uri() { 40 | return uri; 41 | } 42 | 43 | public TestHttpRequest setUri(String uri) { 44 | this.uri = uri; 45 | return this; 46 | } 47 | 48 | @Override 49 | public String method() { 50 | return method; 51 | } 52 | 53 | public TestHttpRequest setMethod(String method) { 54 | this.method = method; 55 | return this; 56 | } 57 | 58 | @Override 59 | public String version() { 60 | return version; 61 | } 62 | 63 | public TestHttpRequest setVersion(String version) { 64 | this.version = version; 65 | return this; 66 | } 67 | 68 | @Override 69 | public String remoteAddress() { 70 | return remoteAddress; 71 | } 72 | 73 | public TestHttpRequest setRemoteAddress(String remoteAddress) { 74 | this.remoteAddress = remoteAddress; 75 | return this; 76 | } 77 | 78 | @Override 79 | public boolean secure() { 80 | return secure; 81 | } 82 | 83 | public TestHttpRequest setSecure(boolean secure) { 84 | this.secure = secure; 85 | return this; 86 | } 87 | 88 | @Override 89 | public String host() { 90 | return host; 91 | } 92 | 93 | public TestHttpRequest setHost(String host) { 94 | this.host = host; 95 | return this; 96 | } 97 | 98 | @Override 99 | public String path() { 100 | return path; 101 | } 102 | 103 | public TestHttpRequest setPath(String path) { 104 | this.path = path; 105 | return this; 106 | } 107 | 108 | @Override 109 | public List acceptLanguages() { 110 | return acceptLanguages; 111 | } 112 | 113 | public TestHttpRequest setAcceptLanguages(List acceptLanguages) { 114 | this.acceptLanguages = acceptLanguages; 115 | return this; 116 | } 117 | 118 | @Override 119 | public List accept() { 120 | return accept; 121 | } 122 | 123 | public TestHttpRequest setAccept(List accept) { 124 | this.accept = accept; 125 | return this; 126 | } 127 | 128 | @Override 129 | public List acceptedTypes() { 130 | return acceptedTypes; 131 | } 132 | 133 | public TestHttpRequest setAcceptedTypes(List acceptedTypes) { 134 | this.acceptedTypes = acceptedTypes; 135 | return this; 136 | } 137 | 138 | @Override 139 | public boolean accepts(String s) { 140 | return accepts; 141 | } 142 | 143 | public TestHttpRequest setAccepts(boolean accepts) { 144 | this.accepts = accepts; 145 | return this; 146 | } 147 | 148 | @Override 149 | public Map queryString() { 150 | return queryString; 151 | } 152 | 153 | public TestHttpRequest setQueryString(Map queryString) { 154 | this.queryString = queryString; 155 | return this; 156 | } 157 | 158 | @Override 159 | public Http.Cookies cookies() { 160 | return cookies; 161 | } 162 | 163 | public TestHttpRequest setCookies(Http.Cookies cookies) { 164 | this.cookies = cookies; 165 | return this; 166 | } 167 | 168 | @Override 169 | public Map headers() { 170 | return headers; 171 | } 172 | 173 | public TestHttpRequest setHeaders(Map headers) { 174 | this.headers = headers; 175 | return this; 176 | } 177 | 178 | @Override 179 | public String toString() { 180 | return toString(this); 181 | } 182 | 183 | public static String toString(Http.Request request) { 184 | return "Http.Request{" + 185 | "body=" + request.body() + 186 | ", uri='" + request.uri() + '\'' + 187 | ", method='" + request.method() + '\'' + 188 | ", version='" + request.version() + '\'' + 189 | ", remoteAddress='" + request.remoteAddress() + '\'' + 190 | ", secure=" + request.secure() + 191 | ", host='" + request.host() + '\'' + 192 | ", path='" + request.path() + '\'' + 193 | ", acceptLanguages=" + request.acceptLanguages() + 194 | ", accept=" + request.accept() + 195 | ", acceptedTypes=" + request.acceptedTypes() + 196 | ", queryString=" + request.queryString() + 197 | ", cookies=" + request.cookies() + 198 | ", headers=" + request.headers() + 199 | '}'; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /forward-cat/forward-cat-james/src/test/java/com/forwardcat/james/ForwardMailetTest.java: -------------------------------------------------------------------------------- 1 | package com.forwardcat.james; 2 | 3 | import com.avaje.ebean.EbeanServer; 4 | import com.forwardcat.common.ProxyMail; 5 | import com.forwardcat.common.User; 6 | import com.google.common.base.Throwables; 7 | import org.apache.james.core.MailImpl; 8 | import org.apache.james.dnsservice.api.DNSService; 9 | import org.apache.mailet.Mail; 10 | import org.apache.mailet.MailAddress; 11 | import org.apache.mailet.MailetConfig; 12 | import org.apache.mailet.MailetContext; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.ArgumentCaptor; 17 | import org.mockito.Captor; 18 | import org.mockito.Mock; 19 | import org.mockito.runners.MockitoJUnitRunner; 20 | import redis.clients.jedis.Jedis; 21 | import redis.clients.jedis.JedisPool; 22 | 23 | import javax.mail.MessagingException; 24 | import javax.mail.Session; 25 | import javax.mail.internet.MimeMessage; 26 | import javax.mail.internet.MimeMultipart; 27 | import java.io.Serializable; 28 | import java.net.InetAddress; 29 | import java.net.UnknownHostException; 30 | import java.util.Collections; 31 | import java.util.Date; 32 | 33 | import static org.hamcrest.CoreMatchers.is; 34 | import static org.hamcrest.CoreMatchers.notNullValue; 35 | import static org.junit.Assert.assertThat; 36 | import static org.mockito.Mockito.*; 37 | 38 | @RunWith(MockitoJUnitRunner.class) 39 | public class ForwardMailetTest { 40 | 41 | ForwardMailet mailet = new ForwardMailet(); 42 | 43 | @Captor ArgumentCaptor mailCaptor; 44 | @Mock ForwardCatResourcesProvider resourcesProvider; 45 | @Mock JedisPool pool; 46 | @Mock Jedis jedis; 47 | @Mock EbeanServer ebeanServer; 48 | @Mock MailetContext context; 49 | 50 | String prefix = "[Forward.cat] "; 51 | MailAddress recipient; 52 | MailAddress userAddress; 53 | 54 | @Before 55 | public void setUp() throws MessagingException, UnknownHostException { 56 | when(pool.getResource()).thenReturn(jedis); 57 | when(resourcesProvider.getPool()).thenReturn(pool); 58 | when(resourcesProvider.getEbeanServer()).thenReturn(ebeanServer); 59 | 60 | DNSService dns = mock(DNSService.class); 61 | when(dns.getLocalHost()).thenReturn(InetAddress.getLocalHost()); 62 | 63 | mailet.setDNSService(dns); 64 | mailet.setResourceProvider(resourcesProvider); 65 | 66 | recipient = new MailAddress("recipient@forward.cat"); 67 | userAddress = new MailAddress("user@mail.com"); 68 | MailetConfig config = mock(MailetConfig.class); 69 | when(config.getInitParameter("sender")).thenReturn("no-reply@forward.cat"); 70 | when(config.getInitParameter("prefix")).thenReturn(prefix); 71 | when(config.getInitParameter("static")).thenReturn("true"); 72 | when(config.getMailetContext()).thenReturn(context); 73 | mailet.init(config); 74 | } 75 | 76 | @Test 77 | public void proxyNotFound_shouldBounceMail() throws MessagingException { 78 | Mail mail = newMail(); 79 | mailet.service(mail); 80 | 81 | verify(pool, atLeastOnce()).returnResource(jedis); 82 | assertThatIsBounced(mail); 83 | assertNoMailHasBeenSent(); 84 | } 85 | 86 | @Test 87 | public void proxyNotFoundAndSenderIgnorable_shouldIgnoreMail() throws MessagingException { 88 | Mail mail = newMail("do-not-reply@mail.com"); 89 | mailet.service(mail); 90 | 91 | assertThatIsIgnored(mail); 92 | assertNoMailHasBeenSent(); 93 | } 94 | 95 | @Test 96 | public void proxyNotFoundAndSenderIsFbUpdate_shouldIgnoreMail() throws MessagingException { 97 | Mail mail = newMail("notification+zj4za=zy2==c@facebookmail.com"); 98 | mailet.service(mail); 99 | 100 | assertThatIsIgnored(mail); 101 | assertNoMailHasBeenSent(); 102 | } 103 | 104 | @Test 105 | public void proxyNotActive_shouldBounceMail() throws MessagingException { 106 | ProxyMail proxy = proxy(); 107 | when(ebeanServer.find(ProxyMail.class, recipient.toString())).thenReturn(proxy); 108 | 109 | Mail mail = newMail(); 110 | mailet.service(mail); 111 | 112 | assertThatIsBounced(mail); 113 | assertNoMailHasBeenSent(); 114 | } 115 | 116 | @Test 117 | public void proxyBlocked_shouldSwallowMail() throws MessagingException { 118 | ProxyMail proxy = proxy(); 119 | proxy.activate(); 120 | proxy.block(); 121 | when(ebeanServer.find(ProxyMail.class, recipient.toString())).thenReturn(proxy); 122 | 123 | Mail mail = newMail(); 124 | mailet.service(mail); 125 | 126 | assertThatIsIgnored(mail); 127 | assertNoMailHasBeenSent(); 128 | } 129 | 130 | @Test 131 | public void proxyActive_shouldForwardMail() throws MessagingException { 132 | ProxyMail proxy = proxy(); 133 | proxy.activate(); 134 | when(ebeanServer.find(ProxyMail.class, recipient.toString())).thenReturn(proxy); 135 | 136 | Mail mail = newMail(); 137 | mailet.service(mail); 138 | 139 | assertThat(mail.getState(), is(Mail.GHOST)); 140 | verify(context).sendMail(mailCaptor.capture()); 141 | 142 | Mail forwardMail = mailCaptor.getValue(); 143 | assertThat(forwardMail, notNullValue()); 144 | } 145 | 146 | private ProxyMail proxy() { 147 | ProxyMail proxy = ProxyMail.create(recipient, userAddress, daysOffset(-2), daysOffset(5), "en"); 148 | proxy.setUser(User.create(userAddress, new Date(), proxy)); 149 | return proxy; 150 | } 151 | 152 | private Mail newMail() throws MessagingException { 153 | return newMail("sender@mail.com"); 154 | } 155 | 156 | private Mail newMail(String sender) throws MessagingException { 157 | MimeMessage message = new MimeMessage(Session.getDefaultInstance(System.getProperties())); 158 | message.setContent(new MimeMultipart()); 159 | message.setSubject("Some subject"); 160 | 161 | return new MailImpl("test email", new MailAddress(sender), Collections.singletonList(recipient), message); 162 | } 163 | 164 | private void assertThatIsBounced(Mail mail) { 165 | Serializable attr = mail.getAttribute(ForwardMailet.BOUNCE_ATTRIBUTE); 166 | assertThat(attr, is("true")); 167 | } 168 | 169 | private void assertThatIsIgnored(Mail mail) { 170 | String attr = mail.getState(); 171 | assertThat(attr, is(Mail.GHOST)); 172 | } 173 | 174 | private void assertNoMailHasBeenSent() { 175 | try { 176 | verify(context, never()).sendMail(any(Mail.class)); 177 | } catch (MessagingException e) { 178 | Throwables.propagate(e); 179 | } 180 | } 181 | 182 | private Date daysOffset(int days) { 183 | return new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * days); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /forward-cat-website/app/views/forward.scala.html: -------------------------------------------------------------------------------- 1 | @import models.DomainManager.buildUrl 2 | @(lang: Lang) 3 | 4 | 5 | 6 | 7 | 8 | @Messages("index.title")(lang) 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 39 | 40 | 41 | 42 |

43 | @navbar(lang) 44 | 45 |
46 |
47 |

@Messages("index.header")(lang)

48 |

@Messages("index.subheader")(lang)

49 |
50 | 51 |
52 |
53 |
54 |
55 |
56 | 57 |
58 | 59 |
60 | 61 |

62 |
63 |
64 |

@@forward.cat

65 |
66 |
67 |
68 |
69 | 70 |
71 | 72 |
73 | 74 |

75 |
76 |
77 |
78 |
79 | 80 | 81 |
82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |

@Messages("index.about.header")(lang)

91 |

@Messages("index.about.content")(lang)

92 |

@Html(Messages("index.about.spam")(lang))

93 |

@Html(Messages("index.about.contact")(lang))

94 |
95 |
96 |

@Messages("index.about.section1.title")(lang)

97 |

@Messages("index.about.section1.content")(lang)

98 | 99 |

@Messages("index.about.section2.title")(lang)

100 |

@Messages("index.about.section2.content")(lang)

101 |
102 |
103 |

@Messages("index.about.section3.title")(lang)

104 |

@Messages("index.about.section3.content")(lang)

105 | 106 |

@Messages("index.about.section4.title")(lang)

107 |

@Messages("index.about.section4.content")(lang)

108 |
109 |
110 |
111 | 112 | 119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /forward-cat-website/conf/messages.en: -------------------------------------------------------------------------------- 1 | navbar.faq = Frequent Questions 2 | navbar.report = Report User 3 | navbar.contact = Contact 4 | 5 | index.title = Forward Cat - Temporary email addresses 6 | index.description = Get a temporary email address that automatically forwards to your real email address. It''s fast, free and anonymous! 7 | 8 | index.header = Get a temporary email address 9 | index.subheader = It''s fast, free and anonymous 10 | index.form.address.label = Your email address: 11 | index.form.address.title = Your email address 12 | index.form.address.content = Insert your real email address. We need it to forward you your emails. 13 | index.form.address.placeholder = Email 14 | index.form.proxy.label = Temporary address: 15 | index.form.proxy.title = Username 16 | index.form.proxy.content = This will be your username 17 | index.form.proxy.placeholder = example 18 | index.form.lang = en 19 | index.form.submit = Let''s do this! 20 | index.about.header = About 21 | index.about.content = Forward Cat is a service that allows you to quickly create a temporary email address that automatically forwards to your real email address. 22 | index.about.spam = Important: we reserve the right to cancel or block your account without any further notice if you are found to be sending spam. If you believe someone is using this service to spam, please report it. 23 | index.about.contact = You can also contact us for any other issue. 24 | index.about.section1.title = Don''t reveal your address 25 | index.about.section1.content = Forward Cat is anonymous- give out a disposable email address when you don''t want to disclose your real address. 26 | index.about.section2.title = Avoid spam 27 | index.about.section2.content = Forward Cat provides a way to stop companies and individuals from continuing to send you emails after you''ve concluded your desired transaction. Keep your address out of sight! 28 | index.about.section3.title = When to use Forward Cat 29 | index.about.section3.content = Use a temporary email address to register for websites that ask for your details in exchange for access, or to sell something on a classifieds site. 30 | index.about.section4.title = Totally private 31 | index.about.section4.content = Forward Cat doesn''t store your emails, and they''re not visible to anyone except you. Your temporary email address will be totally deactivated after its lifespan comes to an end. 32 | index.footer.by = Made by Dani Solà. Special thanks to Sophia Casas. Illustrations by Raúl Vélez. 33 | index.footer.code = Code licensed under the Apache License v2.0 and hosted on GitHub. 34 | index.footer.langs = Other languages: Català - Español 35 | 36 | email_sent.title = Activate your temporary email address! 37 | email_sent.header = Activate your temporary email address! 38 | email_sent.content1 = We have sent you an email, activate your new temporary email address within the next 30 minutes to start using it. 39 | email_sent.content2 = If you don''t see the email, please check your spam folder. 40 | email_sent.back = Back to Forward Cat 41 | 42 | contact.title = How can we help? 43 | contact.header = How can we help? 44 | contact.content = Feel free to contact us about anything! 45 | contact.email = Your email: 46 | contact.message = Message: 47 | contact.submit = Submit 48 | 49 | contact_sent.title = Message sent 50 | contact_sent.header = Message sent 51 | contact_sent.content = Your message has been sent, we'll come back to you shortly. 52 | contact_sent.thanks = Thanks for contacting us! 53 | 54 | report_user.title = Report abuse 55 | report_user.header = Report abuse 56 | report_proxy.content = If you believe that someone is abusing Forward Cat to use it for spamming or any other fraudulent purpose, please fill the following form: 57 | report_user.user = Reported user: 58 | report_user.reason = Content of the message: 59 | report_user.report = Report 60 | 61 | user_reported.title = User reported 62 | user_reported.header = User reported 63 | user_reported.content = The user has been reported and will be investigated soon. However, bear in mind that even if we block his account we cannot stop him from sending more emails. 64 | user_reported.thanks = Thanks for letting us know! 65 | 66 | proxy_created_email.title = Forward Cat - Activate your temporary email address 67 | proxy_created_email.intro = The address {0} is nearly ready: 68 | proxy_created_email.activation_url = https://forward.cat/confirm?p={0}&h={1} 69 | proxy_created_email.activate = Activate temporary address 70 | proxy_created_email.deactivate = To deactivate your temporary email address at any time after it''s been activated, click here. 71 | proxy_created_email.forget_it = If you have not created this temporary email address, just delete this mail and forget about it :) 72 | proxy_created_email.thanks = Thanks for using Forward Cat! 73 | 74 | proxy_created.title = Temporary address activated 75 | proxy_created.header = Congratulations! 76 | proxy_created.content1 = {0} will be active for a week. 77 | proxy_created.content2 = If you want to deactivate it earlier, just click on the deactivation link in the confirmation email you just received. 78 | proxy_created.content3 = We hope you find Forward Cat useful! 79 | 80 | confirm_deletion.title = Deactivating temporary email address 81 | confirm_deletion.header = Deactivating temporary email address 82 | confirm_deletion.content1 = {0} is set to be active until {1}. 83 | confirm_deletion.content2 = Do you want to delete {0}, effective now? 84 | confirm_deletion.deactivate = Deactivate it! 85 | 86 | proxy_deleted.title = Temporary email address deactivated 87 | proxy_deleted.header = Temporary email address deactivated 88 | proxy_deleted.content1 = You won''t receive any more email from this temporary email address. 89 | proxy_deleted.content2 = We hope Forward Cat has been useful! 90 | proxy_deleted.back = Back to Forward Cat 91 | 92 | proxy_expiring_email.title = Your temporary email address will expire soon 93 | proxy_expiring_email.content1 = The temporary address {0} will expire on {1} 94 | proxy_expiring_email.content2 = Click here to extend its life for a few more days. 95 | proxy_expiring_email.content3 = To deactivate your temporary email address whenever you want, click here. 96 | proxy_expiring_email.content4 = Thanks for using Forward Cat! 97 | 98 | proxy_extended.title = Temporary email address successfully extended 99 | proxy_extended.header = Temporary email address successfully extended 100 | proxy_extended.content1 = Your temporary address {0} will be active until {1}. 101 | proxy_extended.content2 = If you want to deactivate it earlier, just click on the deactivation link in the email you just received. 102 | proxy_extended.content3 = Happy forwarding! 103 | 104 | error_page.title = Something went wrong 105 | error_page.header = Something went wrong 106 | error_page.content = We're very sorry, but we cannot fulfill your request. 107 | error_page.back = Back to Forward Cat 108 | 109 | error.too_many_proxies = To fight against spammers, we currently allow a maximum of 3 temporary addresses. If you need more, please contact us explaining why. 110 | 111 | faq.title = Frequent questions 112 | -------------------------------------------------------------------------------- /forward-cat-website/conf/messages.ca: -------------------------------------------------------------------------------- 1 | navbar.faq = Preguntes Freqüents 2 | navbar.report = Reportar Usuari 3 | navbar.contact = Contactar 4 | 5 | index.title = Forward Cat - Adreces temporals d''email 6 | index.description = Crea una adreça temporal d''e-mail que reenvia automàticament els correus a la teva adreça real. És gratuït, ràpid i anònim! 7 | 8 | index.header = Crea una adreça temporal d''e-mail 9 | index.subheader = És gratuït, ràpid i anònim 10 | index.form.address.label = La teva adreça: 11 | index.form.address.title = La teva adreça 12 | index.form.address.content = Introdueix la teva adreça e-mail real. La necessitem per reenviar-te els emails. 13 | index.form.address.placeholder = Adreça 14 | index.form.proxy.label = Nom d''usuari: 15 | index.form.proxy.title = Nom d''usuari 16 | index.form.proxy.content = Serà el nom d''usuari de la teva adreça temporal 17 | index.form.proxy.placeholder = exemple 18 | index.form.lang = ca 19 | index.form.submit = Som-hi! 20 | index.about.header = Quant a 21 | index.about.content = Forward Cat és un servei que permet crear fàcilment una adreça temporal d''email que reenvia automàticament els correus a la teva adreça real. 22 | index.about.spam = Important: ens reservem el dret de cancel·lar o bloquejar la teva compta si descobrim que la fas servir per enviar spam. Si creus que algú està fent servir Forward Cat per enviar spam, reporta-ho si us plau. 23 | index.about.contact = També pots contactar-nos per a qualsevol altre assumpte. 24 | index.about.section1.title = No revelis la teva adreça 25 | index.about.section1.content = Forward Cat és anònim, dóna una adreça temporal quan no vulguis revelar la teva adreça real. 26 | index.about.section2.title = Evita el correu no desitjat 27 | index.about.section2.content = Forward Cat et proporciona una eina per evitar que empreses i persones continuen enviant-te correus un cop hagueu acabat la vostra comunicació. Mantén la teva adreça oculta! 28 | index.about.section3.title = Quan utilitzar Forward Cat 29 | index.about.section3.content = Fes servir una adreça d''email temporal quan et registris a pàgines que demanen que et registris per accedir al contingut, o per vendre alguna cosa a una pàgina d''anuncis classificats. 30 | index.about.section4.title = Totalment privat 31 | index.about.section4.content = Forward Cat no guarda el teu correu, i no és visible per a ningú a part de tu. La teva adreça de correu temporal serà totalment esborrada un cop arribi a la fi de la seva durada. 32 | index.footer.by = Fet per Dani Solà. Agraïment especial a Sophia Casas. Il·lustracions fetes per Raúl Vélez. 33 | index.footer.code = Codi sota llicència Apache License v2.0 hostatjat a GitHub. 34 | index.footer.langs = Altres llengües: English - Español 35 | 36 | email_sent.title = Activeu la vostra adreça temporal! 37 | email_sent.header = Activeu la vostra adreça temporal! 38 | email_sent.content1 = T''hem enviat un email, activa l''adreça temporal en els propers 30 minuts per començar a utilitzar-la. 39 | email_sent.content2 = Si no trobes el correu, comprova que no hagi anat a parar a la carpeta de correu no desitjat. 40 | email_sent.back = Tornar a Forward Cat 41 | 42 | contact.title = Com podem ajudar? 43 | contact.header = Com podem ajudar? 44 | contact.content = No dubtis a contactar-nos pel que sigui! 45 | contact.email = El teu email: 46 | contact.message = Missatge: 47 | contact.submit = Enviar 48 | 49 | contact_sent.title = Missatge enviat 50 | contact_sent.header = Missatge enviat 51 | contact_sent.content = El teu missatge ha estat enviat i respondrem el més aviat possible. 52 | contact_sent.thanks = Gràcies per contactar-nos! 53 | 54 | report_user.title = Reportar us indegut 55 | report_user.header = Reportar us indegut 56 | report_proxy.content = Si crees que algú fa servir Forward Cat per enviar spam o per qualsevol altre ús fraudulent, omple el següent formulari si us plau: 57 | report_user.user = Usuari: 58 | report_user.reason = Contingut del mail: 59 | report_user.report = Reportar 60 | 61 | user_reported.title = Usuari reportat 62 | user_reported.header = Usuari reportat 63 | user_reported.content = L'usuari ha sigut reportat i aviat serà investigat. De totes formes, tingues en compte que encara que bloquegem la seva compta no podem evitar que segueixi enviant spam. 64 | user_reported.thanks = Gràcies per informar-nos! 65 | 66 | proxy_created_email.title = Forward Cat - Activa la teva adreça d''email temporal 67 | proxy_created_email.intro = L''adreça {0} està quasi a punt: 68 | proxy_created_email.activation_url = https://forward.cat/ca/confirm?p={0}&h={1} 69 | proxy_created_email.activate = Activar l''adreça temporal 70 | proxy_created_email.deactivate = Per desactivar l''adreça temporal en qualsevol moment, fes clic aquí. 71 | proxy_created_email.forget_it = Si no has creat aquesta adreça, només cal que eliminis aquest correu i l''oblidis :) 72 | proxy_created_email.thanks = Gràcies per utilitzar Forward Cat! 73 | 74 | proxy_created.title = Adreça temporal activada 75 | proxy_created.header = Felicitats! 76 | proxy_created.content1 = {0} estarà activa durant una setmana. 77 | proxy_created.content2 = Si vols desactivar-la abans, només cal que cliquis a l''enllaç corresponent del correu que acabes de rebre. 78 | proxy_created.content3 = Esperem que Forward Cat et sigui útil! 79 | 80 | confirm_deletion.title = Elimina l''adreça temporal 81 | confirm_deletion.header = Vols eliminar la teva adreça temporal? 82 | confirm_deletion.content1 = {0} estarà activa fins al {1}. 83 | confirm_deletion.content2 = Vols desactivar i eliminar {0} ara? 84 | confirm_deletion.deactivate = Eliminar-la! 85 | 86 | proxy_deleted.title = L''adreça temporal ha sigut eliminada 87 | proxy_deleted.header = L''adreça temporal ha sigut eliminada 88 | proxy_deleted.content1 = No rebràs més correus d''aquesta adreça temporal. 89 | proxy_deleted.content2 = Esperem que Forward Cat t''hagi sigut útil! 90 | proxy_deleted.back = Tornar a Forward Cat 91 | 92 | proxy_expiring_email.title = La teva adreça temporal està a punt de ser eliminada 93 | proxy_expiring_email.content1 = L''adreça temporal {0} serà eliminada el {1} 94 | proxy_expiring_email.content2 = Fes clic aquí per mantenir-la activa uns dies més. 95 | proxy_expiring_email.content3 = Per desactivar l''adreça temporal en qualsevol moment, fes clic aquí. 96 | proxy_expiring_email.content4 = Gràcies per utilitzar Forward Cat! 97 | 98 | proxy_extended.title = Duració de l''adreça temporal correctament perllongada 99 | proxy_extended.header = Duració de l''adreça temporal correctament perllongada 100 | proxy_extended.content1 = L''adreça temporal {0} estarà activa fins a {1}. 101 | proxy_extended.content2 = Si vols desactivar-la abans, només cal que cliquis a l''enllaç corresponent del correu que acabes de rebre. 102 | proxy_extended.content3 = Que tinguis un bon dia! 103 | 104 | error_page.title = Alguna cosa ha fallat 105 | error_page.header = Alguna cosa ha fallat 106 | error_page.content = Ho sentim molt, però la petició no s'ha pogut portar a terme. 107 | error_page.back = Tornar a Forward Cat 108 | 109 | error.too_many_proxies = Per lluitar contra el correu indesitjat, actualment només permetem un màxim de 3 correus temporals. Si en necessites més, envia un correu explicant el perquè. 110 | 111 | faq.title = Preguntes freqüents 112 | -------------------------------------------------------------------------------- /forward-cat-website/conf/messages.es: -------------------------------------------------------------------------------- 1 | navbar.faq = Preguntas Frequentes 2 | navbar.report = Reportar Usuario 3 | navbar.contact = Contactar 4 | 5 | index.title = Forward Cat - Direcciones temporales de email 6 | index.description = Crea una dirección temporal de email que reenvía automáticamente el correo a tu dirección real. ¡Es rápido, gratuito y anónimo! 7 | 8 | index.header = Crea una dirección temporal de correo 9 | index.subheader = Es rápido, gratuito y anónimo 10 | index.form.address.label = Tu dirección: 11 | index.form.address.title = Tu dirección 12 | index.form.address.content = Introduce tu dirección de correo real. La necesitamos para reenviarte tus emails. 13 | index.form.address.placeholder = Dirección 14 | index.form.proxy.label = Nombre de usuario: 15 | index.form.proxy.title = Nombre de usuario 16 | index.form.proxy.content = Será el nombre de usuario de tu dirección de correo temporal 17 | index.form.proxy.placeholder = ejemplo 18 | index.form.lang = es 19 | index.form.submit = ¡Adelante! 20 | index.about.header = Acerca de 21 | index.about.content = Forward Cat es un servicio permite crear fácilmente una dirección de correo temporal que te reenvía automáticamente el correo a tu dirección real. 22 | index.about.spam = Importante: nos reservamos el derecho de cancelar o bloquear tu cuenta si descubrimos que la usas para enviar spam. Si crees que alguien usa Forward Cat para enviar spam, repórtalo por favor. 23 | index.about.contact = También puedes contactarnos para cualquier otro asunto. 24 | index.about.section1.title = No reveles tu dirección 25 | index.about.section1.content = Forward Cat es anónimo, así puedes dar una dirección temporal cuando no quieres revelar tu dirección real. 26 | index.about.section2.title = Evita el correo no deseado 27 | index.about.section2.content = Forward Cat te proporciona una herramienta para evitar que empresas y personas te envíen correos una vez haya acabado vuestra transacción. ¡Mantén oculta tu dirección! 28 | index.about.section3.title = Cuando utilizar Forward Cat 29 | index.about.section3.content = Usa una dirección de email temporal cuando te registres en páginas que exigen tus datos para acceder a los contenidos, o para vender algo en una página de anuncios clasificados. 30 | index.about.section4.title = Totalmente privado 31 | index.about.section4.content = Forward Cat no guarda tus correos y no son visibles para nadie que no seas tú. Tu dirección de correo temporal será destruida completamente cuando sea desactivada. 32 | index.footer.by = Creado por Dani Solà. Agradecimiento especial a Sophia Casas. Ilustraciones realizadas por Raúl Vélez. 33 | index.footer.code = Código bajo licencia Apache License v2.0 hospedado en GitHub. 34 | index.footer.langs = Otros idiomas: Català - English 35 | 36 | email_sent.title = ¡Activa tu dirección temporal! 37 | email_sent.header = ¡Activa tu dirección temporal! 38 | email_sent.content1 = Te hemos enviado un email, activa tu dirección de correo temporal en los siguientes 30 minutos para empezar a usarla. 39 | email_sent.content2 = Si no recibes el email, comprueba que no esté en la carpeta de correo no deseado. 40 | email_sent.back = Volver a Forward Cat 41 | 42 | contact.title = Como podemos ayudar? 43 | contact.header = Como podemos ayudar? 44 | contact.content = No dudes en contactarnos por cualquier cosa! 45 | contact.email = Tu email: 46 | contact.message = Mensaje: 47 | contact.submit = Enviar 48 | 49 | contact_sent.title = Mensaje enviado 50 | contact_sent.header = Mensaje enviado 51 | contact_sent.content = Tu mensaje ha sido enviado y responderemos lo más pronto posible. 52 | contact_sent.thanks = Gracias por contactarnos! 53 | 54 | report_user.title = Reportar uso indebido 55 | report_user.header = Reportar uso indebido 56 | report_proxy.content = Si crees que alguien usa Forward Cat para enviar spam o para cualquier otro uso fraudulento, rellena el siguiente formulario por favor: 57 | report_user.user = Usuario: 58 | report_user.reason = Contenido del mail: 59 | report_user.report = Reportar 60 | 61 | user_reported.title = Usuario reportado 62 | user_reported.header = Usuario reportado 63 | user_reported.content = El usuario ha sido reportado y pronto será investigado. De todas formas, ten en cuenta que aunque bloqueemos su cuenta no podemos evitar que siga enviando spam. 64 | user_reported.thanks = Gracias por informarnos! 65 | 66 | proxy_created_email.title = Forward Cat - Activa tu dirección de correo temporal 67 | proxy_created_email.intro = La dirección {0} está quasi lista: 68 | proxy_created_email.activation_url = https://forward.cat/es/confirm?p={0}&h={1} 69 | proxy_created_email.activate = Activar la dirección temporal 70 | proxy_created_email.deactivate = Para desactivar la dirección temporal en cualquier momento, haz clic aquí. 71 | proxy_created_email.forget_it = Si no has creado esta dirección, elimina este correo y olvídalo :) 72 | proxy_created_email.thanks = ¡Gracias por usar Forward Cat! 73 | 74 | proxy_created.title = Dirección temporal activada 75 | proxy_created.header = ¡Felicidades! 76 | proxy_created.content1 = {0} estará activa durante una semana. 77 | proxy_created.content2 = Si deseas desactivarla antes, solo tienes que hacer clic en el enlace correspondiente en el email que acabas de recibir. 78 | proxy_created.content3 = ¡Esperamos que Forward Cat te resulte útil! 79 | 80 | confirm_deletion.title = Eliminar dirección temporal 81 | confirm_deletion.header = ¿Quieres eliminar tu dirección temporal? 82 | confirm_deletion.content1 = {0} estará activa hasta el {1}. 83 | confirm_deletion.content2 = ¿Quieres eliminar {0} ahora? 84 | confirm_deletion.deactivate = Elimínala! 85 | 86 | proxy_deleted.title = Dirección temporal eliminada 87 | proxy_deleted.header = Dirección temporal eliminada 88 | proxy_deleted.content1 = No recibirás más correos de esta dirección temporal. 89 | proxy_deleted.content2 = ¡Esperamos que Forward Cat te haya resultado útil! 90 | proxy_deleted.back = Volver a Forward Cat 91 | 92 | proxy_expiring_email.title = Su dirección temporal está a punto de ser desactivada 93 | proxy_expiring_email.content1 = La dirección temporal {0} será desactivada y eliminada el {1} 94 | proxy_expiring_email.content2 = Haz clic aquí para mantenerla activa unos días más. 95 | proxy_expiring_email.content3 = Para desactivar la dirección temporal en cualquier momento, haz clic aquí. 96 | proxy_expiring_email.content4 = ¡Gracias por usar Forward Cat! 97 | 98 | proxy_extended.title = Caducidad de la dirección temporal extendida 99 | proxy_extended.header = Caducidad de la dirección temporal extendida 100 | proxy_extended.content1 = La dirección temporal {0} estará activa hasta el {1}. 101 | proxy_extended.content2 = Si deseas desactivarla antes, solo tienes que hacer clic en el enlace correspondiente del email que acabas de recibir. 102 | proxy_extended.content3 = ¡Que tengas un buen día! 103 | 104 | error_page.title = Algo ha fallado 105 | error_page.header = Algo ha fallado 106 | error_page.content = Lo sentimos mucho, pero su petición no se ha podido llevar a cabo. 107 | error_page.back = Volver a Forward Cat 108 | 109 | error.too_many_proxies = Para luchar contra el correo indeseado, actualmente solo permitimos la creación de 3 direcciones temporales. Si necesitas más, envía un correo explicando el porqué. 110 | 111 | faq.title = Preguntas frequentes 112 | --------------------------------------------------------------------------------