001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019package org.apache.james.rrt.lib;
020
021import java.util.Locale;
022import java.util.Map;
023import java.util.regex.Pattern;
024import java.util.regex.PatternSyntaxException;
025
026import javax.inject.Inject;
027import javax.mail.internet.ParseException;
028
029import org.apache.commons.configuration.ConfigurationException;
030import org.apache.commons.configuration.HierarchicalConfiguration;
031import org.apache.james.domainlist.api.DomainList;
032import org.apache.james.domainlist.api.DomainListException;
033import org.apache.james.lifecycle.api.Configurable;
034import org.apache.james.lifecycle.api.LogEnabled;
035import org.apache.james.rrt.api.RecipientRewriteTable;
036import org.apache.james.rrt.api.RecipientRewriteTableException;
037import org.apache.james.rrt.lib.Mapping.Type;
038import org.apache.mailet.MailAddress;
039import org.slf4j.Logger;
040
041import com.google.common.annotations.VisibleForTesting;
042
043/**
044 * 
045 */
046public abstract class AbstractRecipientRewriteTable implements RecipientRewriteTable, LogEnabled, Configurable {
047    // The maximum mappings which will process before throwing exception
048    private int mappingLimit = 10;
049
050    private boolean recursive = true;
051
052    private Logger logger;
053
054    private DomainList domainList;
055
056    @Inject
057    public void setDomainList(DomainList domainList) {
058        this.domainList = domainList;
059    }
060
061    /**
062     * @see org.apache.james.lifecycle.api.Configurable#configure(HierarchicalConfiguration)
063     */
064    public void configure(HierarchicalConfiguration config) throws ConfigurationException {
065        setRecursiveMapping(config.getBoolean("recursiveMapping", true));
066        try {
067            setMappingLimit(config.getInt("mappingLimit", 10));
068        } catch (IllegalArgumentException e) {
069            throw new ConfigurationException(e.getMessage());
070        }
071        doConfigure(config);
072    }
073
074    public void setLog(Logger logger) {
075        this.logger = logger;
076    }
077
078    /**
079     * Override to handle config
080     * 
081     * @param conf
082     * @throws ConfigurationException
083     */
084    protected void doConfigure(HierarchicalConfiguration conf) throws ConfigurationException {
085
086    }
087
088    public void setRecursiveMapping(boolean recursive) {
089        this.recursive = recursive;
090    }
091
092    /**
093     * Set the mappingLimit
094     * 
095     * @param mappingLimit
096     *            the mappingLimit
097     * @throws IllegalArgumentException
098     *             get thrown if mappingLimit smaller then 1 is used
099     */
100    public void setMappingLimit(int mappingLimit) throws IllegalArgumentException {
101        if (mappingLimit < 1)
102            throw new IllegalArgumentException("The minimum mappingLimit is 1");
103        this.mappingLimit = mappingLimit;
104    }
105
106    /**
107     * @see org.apache.james.rrt.api.RecipientRewriteTable#getMappings(String,
108     *      String)
109     */
110    public Mappings getMappings(String user, String domain) throws ErrorMappingException, RecipientRewriteTableException {
111        return getMappings(user, domain, mappingLimit);
112    }
113
114    public Mappings getMappings(String user, String domain, int mappingLimit) throws ErrorMappingException, RecipientRewriteTableException {
115
116        // We have to much mappings throw ErrorMappingException to avoid
117        // infinity loop
118        if (mappingLimit == 0)
119            throw new ErrorMappingException("554 Too many mappings to process");
120
121        Mappings targetMappings = mapAddress(user, domain);
122
123        // Only non-null mappings are translated
124        if (targetMappings != null) {
125            if (targetMappings.contains(Type.Error)) {
126                throw new ErrorMappingException(targetMappings.getError().getErrorMessage());
127            } else {
128                MappingsImpl.Builder mappings = MappingsImpl.builder();
129
130                for (String target : targetMappings.asStrings()) {
131                    if (target.startsWith(RecipientRewriteTable.REGEX_PREFIX)) {
132                        try {
133                            target = RecipientRewriteTableUtil.regexMap(new MailAddress(user, domain), target);
134                        } catch (PatternSyntaxException e) {
135                            getLogger().error("Exception during regexMap processing: ", e);
136                        } catch (ParseException e) {
137                            // should never happen
138                            getLogger().error("Exception during regexMap processing: ", e);
139                        }
140                    } else if (target.startsWith(RecipientRewriteTable.ALIASDOMAIN_PREFIX)) {
141                        target = user + "@" + target.substring(RecipientRewriteTable.ALIASDOMAIN_PREFIX.length());
142                    }
143
144                    if (target == null)
145                        continue;
146
147                    String buf = "Valid virtual user mapping " + user + "@" + domain + " to " + target;
148                    getLogger().debug(buf);
149
150                    if (recursive) {
151
152                        String userName;
153                        String domainName;
154                        String args[] = target.split("@");
155
156                        if (args != null && args.length > 1) {
157
158                            userName = args[0];
159                            domainName = args[1];
160                        } else {
161                            // TODO Is that the right todo here?
162                            userName = target;
163                            domainName = domain;
164                        }
165
166                        // Check if the returned mapping is the same as the
167                        // input. If so return null to avoid loops
168                        if (userName.equalsIgnoreCase(user) && domainName.equalsIgnoreCase(domain)) {
169                            return null;
170                        }
171
172                        Mappings childMappings = getMappings(userName, domainName, mappingLimit - 1);
173
174                        if (childMappings == null) {
175                            // add mapping
176                            mappings.add(target);
177                        } else {
178                            mappings = mappings.addAll(childMappings);
179                        }
180
181                    } else {
182                        mappings.add(target);
183                    }
184                }
185                return mappings.build();
186            }
187        }
188
189        return null;
190    }
191
192    /**
193     * @see org.apache.james.rrt.api.RecipientRewriteTable#addRegexMapping(java.lang.String,
194     *      java.lang.String, java.lang.String)
195     */
196    public void addRegexMapping(String user, String domain, String regex) throws RecipientRewriteTableException {
197        try {
198            Pattern.compile(regex);
199        } catch (PatternSyntaxException e) {
200            throw new RecipientRewriteTableException("Invalid regex: " + regex, e);
201        }
202
203        checkMapping(user, domain, regex);
204        getLogger().info("Add regex mapping => " + regex + " for user: " + user + " domain: " + domain);
205        addMappingInternal(user, domain, RecipientRewriteTable.REGEX_PREFIX + regex);
206
207    }
208
209    /**
210     * @see org.apache.james.rrt.api.RecipientRewriteTable#removeRegexMapping(java.lang.String,
211     *      java.lang.String, java.lang.String)
212     */
213    public void removeRegexMapping(String user, String domain, String regex) throws RecipientRewriteTableException {
214        getLogger().info("Remove regex mapping => " + regex + " for user: " + user + " domain: " + domain);
215        removeMappingInternal(user, domain, RecipientRewriteTable.REGEX_PREFIX + regex);
216    }
217
218    /**
219     * @see org.apache.james.rrt.api.RecipientRewriteTable#addAddressMapping(java.lang.String,
220     *      java.lang.String, java.lang.String)
221     */
222    public void addAddressMapping(String user, String domain, String address) throws RecipientRewriteTableException {
223        if (address.indexOf('@') < 0) {
224            try {
225                address = address + "@" + domainList.getDefaultDomain();
226            } catch (DomainListException e) {
227                throw new RecipientRewriteTableException("Unable to retrieve default domain", e);
228            }
229        }
230        try {
231            new MailAddress(address);
232        } catch (ParseException e) {
233            throw new RecipientRewriteTableException("Invalid emailAddress: " + address, e);
234        }
235        checkMapping(user, domain, address);
236        getLogger().info("Add address mapping => " + address + " for user: " + user + " domain: " + domain);
237        addMappingInternal(user, domain, address);
238
239    }
240
241    /**
242     * @see org.apache.james.rrt.api.RecipientRewriteTable#removeAddressMapping(java.lang.String,
243     *      java.lang.String, java.lang.String)
244     */
245    public void removeAddressMapping(String user, String domain, String address) throws RecipientRewriteTableException {
246        if (address.indexOf('@') < 0) {
247            try {
248                address = address + "@" + domainList.getDefaultDomain();
249            } catch (DomainListException e) {
250                throw new RecipientRewriteTableException("Unable to retrieve default domain", e);
251            }
252        }
253        getLogger().info("Remove address mapping => " + address + " for user: " + user + " domain: " + domain);
254        removeMappingInternal(user, domain, address);
255    }
256
257    /**
258     * @see org.apache.james.rrt.api.RecipientRewriteTable#addErrorMapping(java.lang.String,
259     *      java.lang.String, java.lang.String)
260     */
261    public void addErrorMapping(String user, String domain, String error) throws RecipientRewriteTableException {
262        checkMapping(user, domain, error);
263        getLogger().info("Add error mapping => " + error + " for user: " + user + " domain: " + domain);
264        addMappingInternal(user, domain, RecipientRewriteTable.ERROR_PREFIX + error);
265
266    }
267
268    /**
269     * @see org.apache.james.rrt.api.RecipientRewriteTable#removeErrorMapping(java.lang.String,
270     *      java.lang.String, java.lang.String)
271     */
272    public void removeErrorMapping(String user, String domain, String error) throws RecipientRewriteTableException {
273        getLogger().info("Remove error mapping => " + error + " for user: " + user + " domain: " + domain);
274        removeMappingInternal(user, domain, RecipientRewriteTable.ERROR_PREFIX + error);
275    }
276
277    /**
278     * @see org.apache.james.rrt.api.RecipientRewriteTable#addMapping(java.lang.String,
279     *      java.lang.String, java.lang.String)
280     */
281    public void addMapping(String user, String domain, String mapping) throws RecipientRewriteTableException {
282
283        String map = mapping.toLowerCase(Locale.US);
284
285        if (map.startsWith(RecipientRewriteTable.ERROR_PREFIX)) {
286            addErrorMapping(user, domain, map.substring(RecipientRewriteTable.ERROR_PREFIX.length()));
287        } else if (map.startsWith(RecipientRewriteTable.REGEX_PREFIX)) {
288            addRegexMapping(user, domain, map.substring(RecipientRewriteTable.REGEX_PREFIX.length()));
289        } else if (map.startsWith(RecipientRewriteTable.ALIASDOMAIN_PREFIX)) {
290            if (user != null)
291                throw new RecipientRewriteTableException("User must be null for aliasDomain mappings");
292            addAliasDomainMapping(domain, map.substring(RecipientRewriteTable.ALIASDOMAIN_PREFIX.length()));
293        } else {
294            addAddressMapping(user, domain, map);
295        }
296
297    }
298
299    /**
300     * @see org.apache.james.rrt.api.RecipientRewriteTable#removeMapping(java.lang.String,
301     *      java.lang.String, java.lang.String)
302     */
303    public void removeMapping(String user, String domain, String mapping) throws RecipientRewriteTableException {
304
305        String map = mapping.toLowerCase(Locale.US);
306
307        if (map.startsWith(RecipientRewriteTable.ERROR_PREFIX)) {
308            removeErrorMapping(user, domain, map.substring(RecipientRewriteTable.ERROR_PREFIX.length()));
309        } else if (map.startsWith(RecipientRewriteTable.REGEX_PREFIX)) {
310            removeRegexMapping(user, domain, map.substring(RecipientRewriteTable.REGEX_PREFIX.length()));
311        } else if (map.startsWith(RecipientRewriteTable.ALIASDOMAIN_PREFIX)) {
312            if (user != null)
313                throw new RecipientRewriteTableException("User must be null for aliasDomain mappings");
314            removeAliasDomainMapping(domain, map.substring(RecipientRewriteTable.ALIASDOMAIN_PREFIX.length()));
315        } else {
316            removeAddressMapping(user, domain, map);
317        }
318
319    }
320
321    /**
322     * @see org.apache.james.rrt.api.RecipientRewriteTable#getAllMappings()
323     */
324    public Map<String, Mappings> getAllMappings() throws RecipientRewriteTableException {
325        int count = 0;
326        Map<String, Mappings> mappings = getAllMappingsInternal();
327
328        if (mappings != null) {
329            count = mappings.size();
330        }
331        getLogger().debug("Retrieve all mappings. Mapping count: " + count);
332        return mappings;
333    }
334
335    /**
336     * @see org.apache.james.rrt.api.RecipientRewriteTable#getUserDomainMappings(java.lang.String,
337     *      java.lang.String)
338     */
339    public Mappings getUserDomainMappings(String user, String domain) throws RecipientRewriteTableException {
340        return getUserDomainMappingsInternal(user, domain);
341    }
342
343    /**
344     * @see org.apache.james.rrt.api.RecipientRewriteTable#addAliasDomainMapping(java.lang.String,
345     *      java.lang.String)
346     */
347    public void addAliasDomainMapping(String aliasDomain, String realDomain) throws RecipientRewriteTableException {
348        getLogger().info("Add domain mapping: " + aliasDomain + " => " + realDomain);
349        addMappingInternal(null, aliasDomain, RecipientRewriteTable.ALIASDOMAIN_PREFIX + realDomain);
350    }
351
352    /**
353     * @see org.apache.james.rrt.api.RecipientRewriteTable#removeAliasDomainMapping(java.lang.String,
354     *      java.lang.String)
355     */
356    public void removeAliasDomainMapping(String aliasDomain, String realDomain) throws RecipientRewriteTableException {
357        getLogger().info("Remove domain mapping: " + aliasDomain + " => " + realDomain);
358        removeMappingInternal(null, aliasDomain, RecipientRewriteTable.ALIASDOMAIN_PREFIX + realDomain);
359    }
360
361    protected Logger getLogger() {
362        return logger;
363    }
364
365    /**
366     * Add new mapping
367     * 
368     * @param user
369     *            the user
370     * @param domain
371     *            the domain
372     * @param mapping
373     *            the mapping
374     * @throws RecipientRewriteTableException
375     */
376    protected abstract void addMappingInternal(String user, String domain, String mapping) throws RecipientRewriteTableException;
377
378    /**
379     * Remove mapping
380     * 
381     * @param user
382     *            the user
383     * @param domain
384     *            the domain
385     * @param mapping
386     *            the mapping
387     * @throws RecipientRewriteTableException
388     */
389    protected abstract void removeMappingInternal(String user, String domain, String mapping) throws RecipientRewriteTableException;
390
391    /**
392     * Return Collection of all mappings for the given username and domain
393     * 
394     * @param user
395     *            the user
396     * @param domain
397     *            the domain
398     * @return Collection which hold the mappings
399     */
400    protected abstract Mappings getUserDomainMappingsInternal(String user, String domain) throws RecipientRewriteTableException;
401
402    /**
403     * Return a Map which holds all Mappings
404     * 
405     * @return Map
406     */
407    protected abstract Map<String, Mappings> getAllMappingsInternal() throws RecipientRewriteTableException;
408
409    /**
410     * Override to map virtual recipients to real recipients, both local and
411     * non-local. Each key in the provided map corresponds to a potential
412     * virtual recipient, stored as a <code>MailAddress</code> object.
413     * 
414     * Translate virtual recipients to real recipients by mapping a string
415     * containing the address of the real recipient as a value to a key. Leave
416     * the value <code>null<code>
417     * if no mapping should be performed. Multiple recipients may be specified by delineating
418     * the mapped string with commas, semi-colons or colons.
419     * 
420     * @param user
421     *            the mapping of virtual to real recipients, as
422     *            <code>MailAddress</code>es to <code>String</code>s.
423     */
424    protected abstract String mapAddressInternal(String user, String domain) throws RecipientRewriteTableException;
425
426    /**
427     * Get all mappings for the given user and domain. If a aliasdomain mapping
428     * was found get sure it is in the map as first mapping.
429     * 
430     * @param user
431     *            the username
432     * @param domain
433     *            the domain
434     * @return the mappings
435     */
436    private Mappings mapAddress(String user, String domain) throws RecipientRewriteTableException {
437
438        String mappings = mapAddressInternal(user, domain);
439
440        if (mappings != null) {
441            return sortMappings(MappingsImpl.fromRawString(mappings));
442        } else {
443            return null;
444        }
445    }
446
447    @VisibleForTesting static Mappings sortMappings(Mappings mappings) {
448        if (mappings.contains(Mapping.Type.Domain)) {
449            return
450                    MappingsImpl.builder()
451                        .addAll(mappings.select(Mapping.Type.Domain))
452                        .addAll(mappings.exclude(Mapping.Type.Domain))
453                        .build();
454        } else {
455            return mappings;
456        }
457    }
458
459    private void checkMapping(String user, String domain, String mapping) throws RecipientRewriteTableException {
460        Mappings mappings = getUserDomainMappings(user, domain);
461        if (mappings != null && mappings.contains(mapping)) {
462            throw new RecipientRewriteTableException("Mapping " + mapping + " for user " + user + " domain " + domain + " already exist!");
463        }
464    }
465
466    /**
467     * Return user String for the given argument.
468     * If give value is null, return a wildcard.
469     * 
470     * @param user the given user String
471     * @return fixedUser the fixed user String
472     */
473    protected String getFixedUser(String user) {
474        if (user != null) {
475            if (user.equals(WILDCARD) || !user.contains("@")) {
476                return user;
477            } else {
478                throw new IllegalArgumentException("Invalid user: " + user);
479            }
480        } else {
481            return WILDCARD;
482        }
483    }
484
485    /**
486     * Fix the domain for the given argument.
487     * If give value is null, return a wildcard.
488     * 
489     * @param domain the given domain String
490     * @return fixedDomain the fixed domain String
491     */
492    protected String getFixedDomain(String domain) {
493        if (domain != null) {
494            if (domain.equals(WILDCARD) || !domain.contains("@")) {
495                return domain;
496            } else {
497                throw new IllegalArgumentException("Invalid domain: " + domain);
498            }
499        } else {
500            return WILDCARD;
501        }
502    }
503
504}