1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.apache.james.smtpserver.core.filter.fastfail;
21
22 import java.io.InputStream;
23 import java.sql.Connection;
24 import java.sql.DatabaseMetaData;
25 import java.sql.PreparedStatement;
26 import java.sql.ResultSet;
27 import java.sql.SQLException;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.Map;
33 import java.util.StringTokenizer;
34 import java.sql.Timestamp;
35
36 import org.apache.avalon.cornerstone.services.datasources.DataSourceSelector;
37 import org.apache.avalon.excalibur.datasource.DataSourceComponent;
38 import org.apache.avalon.framework.activity.Initializable;
39 import org.apache.avalon.framework.configuration.Configurable;
40 import org.apache.avalon.framework.configuration.Configuration;
41 import org.apache.avalon.framework.configuration.ConfigurationException;
42 import org.apache.avalon.framework.logger.AbstractLogEnabled;
43 import org.apache.avalon.framework.service.ServiceException;
44 import org.apache.avalon.framework.service.ServiceManager;
45 import org.apache.avalon.framework.service.Serviceable;
46
47 import org.apache.james.api.dnsservice.DNSService;
48 import org.apache.james.api.dnsservice.util.NetMatcher;
49 import org.apache.james.dsn.DSNStatus;
50 import org.apache.james.services.FileSystem;
51 import org.apache.james.smtpserver.CommandHandler;
52 import org.apache.james.smtpserver.SMTPSession;
53 import org.apache.james.util.TimeConverter;
54 import org.apache.james.util.sql.JDBCUtil;
55 import org.apache.james.util.sql.SqlResources;
56 import org.apache.mailet.MailAddress;
57
58
59
60
61 public class GreylistHandler extends AbstractLogEnabled implements
62 CommandHandler, Configurable, Serviceable, Initializable {
63
64 private DataSourceSelector datasources = null;
65
66 private DataSourceComponent datasource = null;
67
68 private FileSystem fileSystem = null;
69
70
71 private long tempBlockTime = 3600000;
72
73
74 private long autoWhiteListLifeTime = 3110400000L;
75
76
77 private long unseenLifeTime = 14400000;
78
79 private String selectQuery;
80
81 private String insertQuery;
82
83 private String deleteQuery;
84
85 private String deleteAutoWhiteListQuery;
86
87 private String updateQuery;
88
89
90
91
92 private SqlResources sqlQueries = new SqlResources();
93
94
95
96
97 private String sqlFileUrl;
98
99
100
101
102 private Map sqlParameters = new HashMap();
103
104
105
106
107 private String repositoryPath;
108
109 private DNSService dnsServer;
110
111 private NetMatcher wNetworks;
112
113
114
115
116 public void configure(Configuration handlerConfiguration) throws ConfigurationException {
117 Configuration configTemp = handlerConfiguration.getChild("tempBlockTime", false);
118 if (configTemp != null) {
119 try {
120 setTempBlockTime(configTemp.getValue());
121
122 } catch (NumberFormatException e) {
123 throw new ConfigurationException(e.getMessage());
124 }
125 }
126
127 Configuration configAutoWhiteList = handlerConfiguration.getChild("autoWhiteListLifeTime", false);
128 if (configAutoWhiteList != null) {
129 try {
130 setAutoWhiteListLifeTime(configAutoWhiteList.getValue());
131 } catch (NumberFormatException e) {
132 throw new ConfigurationException(e.getMessage());
133 }
134 }
135
136 Configuration configUnseen = handlerConfiguration.getChild("unseenLifeTime", false);
137 if (configUnseen != null) {
138 try {
139 setUnseenLifeTime(configUnseen.getValue());
140 } catch (NumberFormatException e) {
141 throw new ConfigurationException(e.getMessage());
142 }
143 }
144
145 Configuration configRepositoryPath = handlerConfiguration.getChild("repositoryPath", false);
146 if (configRepositoryPath != null) {
147 setRepositoryPath(configRepositoryPath.getValue());
148 } else {
149 throw new ConfigurationException("repositoryPath is not configured");
150 }
151
152
153 Configuration sFile = handlerConfiguration.getChild("sqlFile", false);
154 if (sFile != null) {
155 setSqlFileUrl(sFile.getValue());
156 if (!sqlFileUrl.startsWith("file://")) {
157 throw new ConfigurationException(
158 "Malformed sqlFile - Must be of the format \"file://<filename>\".");
159 }
160 } else {
161 throw new ConfigurationException("sqlFile is not configured");
162 }
163
164 Configuration whitelistedNetworks = handlerConfiguration.getChild("whitelistedNetworks", false);
165 if (whitelistedNetworks != null) {
166 Collection nets = whitelistedNetworks(whitelistedNetworks.getValue());
167
168 if (nets != null) {
169 wNetworks = new NetMatcher(nets,dnsServer);
170 getLogger().info("Whitelisted addresses: " + wNetworks.toString());
171 }
172 }
173 }
174
175
176
177
178 public void initialize() throws Exception {
179 setDataSource(initDataSource(repositoryPath));
180 initSqlQueries(datasource.getConnection(), sqlFileUrl);
181
182
183 createTable(datasource.getConnection(), "greyListTableName", "createGreyListTable");
184 }
185
186
187
188
189 public void service(ServiceManager serviceMan) throws ServiceException {
190 setDataSources((DataSourceSelector) serviceMan.lookup(DataSourceSelector.ROLE));
191 setDnsServer((DNSService) serviceMan.lookup(DNSService.ROLE));
192 setFileSystem((FileSystem) serviceMan.lookup(FileSystem.ROLE));
193 }
194
195
196
197
198
199
200
201 public void setDnsServer(DNSService dnsServer) {
202 this.dnsServer = dnsServer;
203 }
204
205
206
207
208
209
210
211 public void setSqlFileUrl(String sqlFileUrl) {
212 this.sqlFileUrl = sqlFileUrl;
213 }
214
215
216
217
218
219
220
221 public void setRepositoryPath(String repositoryPath) {
222 this.repositoryPath = repositoryPath;
223 }
224
225
226
227
228
229
230
231 public void setDataSources(DataSourceSelector datasources) {
232 this.datasources = datasources;
233 }
234
235
236
237
238
239
240 private void setFileSystem(FileSystem system) {
241 this.fileSystem = system;
242 }
243
244
245
246
247
248
249
250
251 public void setDataSource(DataSourceComponent datasource) {
252 this.datasource = datasource;
253 }
254
255
256
257
258
259
260
261 public void setTempBlockTime(String tempBlockTime) {
262 this.tempBlockTime = TimeConverter.getMilliSeconds(tempBlockTime);
263 }
264
265
266
267
268
269
270
271
272 public void setAutoWhiteListLifeTime(String autoWhiteListLifeTime) {
273 this.autoWhiteListLifeTime = TimeConverter.getMilliSeconds(autoWhiteListLifeTime);
274 }
275
276
277
278
279
280
281
282
283 public void setUnseenLifeTime(String unseenLifeTime) {
284 this.unseenLifeTime = TimeConverter.getMilliSeconds(unseenLifeTime);
285 }
286
287
288
289
290 public void onCommand(SMTPSession session) {
291 if (!session.isRelayingAllowed() && !(session.isAuthRequired() && session.getUser() != null)) {
292
293 if ((wNetworks == null) || (!wNetworks.matchInetNetwork(session.getRemoteIPAddress()))) {
294 doGreyListCheck(session, session.getCommandArgument());
295 } else {
296 getLogger().info("IpAddress " + session.getRemoteIPAddress() + " is whitelisted. Skip greylisting.");
297 }
298 } else {
299 getLogger().info("IpAddress " + session.getRemoteIPAddress() + " is allowed to send. Skip greylisting.");
300 }
301 }
302
303
304
305
306
307
308
309
310
311
312 private void doGreyListCheck(SMTPSession session, String argument) {
313 String recip = "";
314 String sender = "";
315 MailAddress recipAddress = (MailAddress) session.getState().get(SMTPSession.CURRENT_RECIPIENT);
316 MailAddress senderAddress = (MailAddress) session.getState().get(SMTPSession.SENDER);
317
318 if (recipAddress != null) recip = recipAddress.toString();
319 if (senderAddress != null) sender = senderAddress.toString();
320
321 long time = System.currentTimeMillis();
322 String ipAddress = session.getRemoteIPAddress();
323
324 try {
325 long createTimeStamp = 0;
326 int count = 0;
327
328
329 Iterator data = getGreyListData(datasource.getConnection(), ipAddress, sender, recip);
330
331 if (data.hasNext()) {
332 createTimeStamp = Long.parseLong(data.next().toString());
333 count = Integer.parseInt(data.next().toString());
334 }
335
336 getLogger().debug("Triplet " + ipAddress + " | " + sender + " | " + recip +" -> TimeStamp: " + createTimeStamp);
337
338
339
340 if (createTimeStamp > 0) {
341 long acceptTime = createTimeStamp + tempBlockTime;
342
343 if ((time < acceptTime) && (count == 0)) {
344 String responseString = "451 " + DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_DIR_SERVER)
345 + " Temporary rejected: Reconnect to fast. Please try again later";
346
347
348 session.writeResponse(responseString);
349 session.setStopHandlerProcessing(true);
350
351 } else {
352
353 getLogger().debug("Update triplet " + ipAddress + " | " + sender + " | " + recip + " -> timestamp: " + time);
354
355
356 updateTriplet(datasource.getConnection(), ipAddress, sender, recip, count, time);
357
358 }
359 } else {
360 getLogger().debug("New triplet " + ipAddress + " | " + sender + " | " + recip );
361
362
363 insertTriplet(datasource.getConnection(), ipAddress, sender, recip, count, time);
364
365
366 String responseString = "451 " + DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_DIR_SERVER)
367 + " Temporary rejected: Please try again later";
368
369 session.writeResponse(responseString);
370 session.setStopHandlerProcessing(true);
371 }
372
373
374 if (Math.random() > 0.99) {
375
376
377 getLogger().debug("Delete old entries");
378
379 cleanupAutoWhiteListGreyList(datasource.getConnection(),(time - autoWhiteListLifeTime));
380 cleanupGreyList(datasource.getConnection(), (time - unseenLifeTime));
381 }
382
383 } catch (SQLException e) {
384
385 getLogger().error("Error on SQLquery: " + e.getMessage());
386 }
387 }
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404 private Iterator getGreyListData(Connection conn, String ipAddress,
405 String sender, String recip) throws SQLException {
406 Collection data = new ArrayList(2);
407 PreparedStatement mappingStmt = null;
408 try {
409 mappingStmt = conn.prepareStatement(selectQuery);
410 ResultSet mappingRS = null;
411 try {
412 mappingStmt.setString(1, ipAddress);
413 mappingStmt.setString(2, sender);
414 mappingStmt.setString(3, recip);
415 mappingRS = mappingStmt.executeQuery();
416
417 if (mappingRS.next()) {
418 data.add(String.valueOf(mappingRS.getTimestamp(1).getTime()));
419 data.add(String.valueOf(mappingRS.getInt(2)));
420 }
421 } finally {
422 theJDBCUtil.closeJDBCResultSet(mappingRS);
423 }
424 } finally {
425 theJDBCUtil.closeJDBCStatement(mappingStmt);
426 theJDBCUtil.closeJDBCConnection(conn);
427 }
428 return data.iterator();
429 }
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448 private void insertTriplet(Connection conn, String ipAddress,
449 String sender, String recip, int count, long createTime)
450 throws SQLException {
451
452 PreparedStatement mappingStmt = null;
453
454 try {
455 mappingStmt = conn.prepareStatement(insertQuery);
456
457 mappingStmt.setString(1, ipAddress);
458 mappingStmt.setString(2, sender);
459 mappingStmt.setString(3, recip);
460 mappingStmt.setInt(4, count);
461 mappingStmt.setTimestamp(5, new Timestamp(createTime));
462 mappingStmt.executeUpdate();
463 } finally {
464 theJDBCUtil.closeJDBCStatement(mappingStmt);
465 theJDBCUtil.closeJDBCConnection(conn);
466 }
467 }
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487 private void updateTriplet(Connection conn, String ipAddress,
488 String sender, String recip, int count, long time)
489 throws SQLException {
490
491 PreparedStatement mappingStmt = null;
492
493 try {
494 mappingStmt = conn.prepareStatement(updateQuery);
495 mappingStmt.setTimestamp(1, new Timestamp(time));
496 mappingStmt.setInt(2, (count + 1));
497 mappingStmt.setString(3, ipAddress);
498 mappingStmt.setString(4, sender);
499 mappingStmt.setString(5, recip);
500 mappingStmt.executeUpdate();
501 } finally {
502 theJDBCUtil.closeJDBCStatement(mappingStmt);
503 theJDBCUtil.closeJDBCConnection(conn);
504 }
505 }
506
507
508
509
510
511
512
513
514
515
516 private DataSourceComponent initDataSource(String repositoryPath)
517 throws ServiceException, SQLException {
518
519 int stindex = repositoryPath.indexOf("://") + 3;
520 String datasourceName = repositoryPath.substring(stindex);
521
522 return (DataSourceComponent) datasources.select(datasourceName);
523 }
524
525
526
527
528
529
530
531
532
533
534 private void cleanupAutoWhiteListGreyList(Connection conn, long time)
535 throws SQLException {
536 PreparedStatement mappingStmt = null;
537
538 try {
539 mappingStmt = conn.prepareStatement(deleteAutoWhiteListQuery);
540
541 mappingStmt.setTimestamp(1, new Timestamp(time));
542
543 mappingStmt.executeUpdate();
544 } finally {
545 theJDBCUtil.closeJDBCStatement(mappingStmt);
546 theJDBCUtil.closeJDBCConnection(conn);
547 }
548 }
549
550
551
552
553
554
555
556
557
558
559 private void cleanupGreyList(Connection conn, long time)
560 throws SQLException {
561 PreparedStatement mappingStmt = null;
562
563 try {
564 mappingStmt = conn.prepareStatement(deleteQuery);
565
566 mappingStmt.setTimestamp(1, new Timestamp(time));
567
568 mappingStmt.executeUpdate();
569 } finally {
570 theJDBCUtil.closeJDBCStatement(mappingStmt);
571 theJDBCUtil.closeJDBCConnection(conn);
572 }
573 }
574
575
576
577
578 private final JDBCUtil theJDBCUtil = new JDBCUtil() {
579 protected void delegatedLog(String logString) {
580 getLogger().debug("JDBCVirtualUserTable: " + logString);
581 }
582 };
583
584
585
586
587
588
589
590
591
592
593
594
595 public void initSqlQueries(Connection conn, String sqlFileUrl)
596 throws Exception {
597 try {
598
599 InputStream sqlFile = null;
600
601 try {
602 sqlFile = fileSystem.getResource(sqlFileUrl);
603 sqlFileUrl = null;
604 } catch (Exception e) {
605 getLogger().fatalError(e.getMessage(), e);
606 throw e;
607 }
608
609 sqlQueries.init(sqlFile, "GreyList", conn, sqlParameters);
610
611 selectQuery = sqlQueries.getSqlString("selectQuery", true);
612 insertQuery = sqlQueries.getSqlString("insertQuery", true);
613 deleteQuery = sqlQueries.getSqlString("deleteQuery", true);
614 deleteAutoWhiteListQuery = sqlQueries.getSqlString("deleteAutoWhitelistQuery", true);
615 updateQuery = sqlQueries.getSqlString("updateQuery", true);
616
617 } finally {
618 theJDBCUtil.closeJDBCConnection(conn);
619 }
620 }
621
622
623
624
625
626
627
628
629
630
631
632
633
634 private boolean createTable(Connection conn, String tableNameSqlStringName,
635 String createSqlStringName) throws SQLException {
636 String tableName = sqlQueries.getSqlString(tableNameSqlStringName, true);
637
638 DatabaseMetaData dbMetaData = conn.getMetaData();
639
640
641 if (theJDBCUtil.tableExists(dbMetaData, tableName)) {
642 return false;
643 }
644
645 PreparedStatement createStatement = null;
646
647 try {
648 createStatement = conn.prepareStatement(sqlQueries.getSqlString(createSqlStringName, true));
649 createStatement.execute();
650
651 StringBuffer logBuffer = null;
652 logBuffer = new StringBuffer(64).append("Created table '").append(tableName)
653 .append("' using sqlResources string '")
654 .append(createSqlStringName).append("'.");
655 getLogger().info(logBuffer.toString());
656
657 } finally {
658 theJDBCUtil.closeJDBCStatement(createStatement);
659 }
660 return true;
661 }
662
663
664
665
666
667
668
669
670
671 private Collection whitelistedNetworks(String networks) {
672 Collection wNetworks = null;
673 StringTokenizer st = new StringTokenizer(networks, ", ", false);
674 wNetworks = new ArrayList();
675
676 while (st.hasMoreTokens())
677 wNetworks.add(st.nextToken());
678 return wNetworks;
679 }
680
681 public Collection getImplCommands() {
682 Collection c = new ArrayList();
683 c.add("RCPT");
684 return c;
685 }
686 }