ant-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From co...@apache.org
Subject cvs commit: jakarta-ant/src/main/org/apache/tools/ant/taskdefs Manifest.java Jar.java Zip.java
Date Sat, 28 Jul 2001 14:11:41 GMT
conor       01/07/28 07:11:41

  Modified:    src/main/org/apache/tools/ant/taskdefs Jar.java Zip.java
  Added:       src/main/org/apache/tools/ant/taskdefs Manifest.java
  Log:
  Add new Manifest handling class.
  Manifest entries longer than 72 bytes are wrapped according to the spec
  Illegal manifest files now cause errors
  Standard fields are provided by default and merged in
  Manifest set by attribute and any added in META-INF/Manifest.mf are merged
  
  PR:	1193, 2295
  
  Revision  Changes    Path
  1.19      +116 -31   jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Jar.java
  
  Index: Jar.java
  ===================================================================
  RCS file: /home/cvs/jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Jar.java,v
  retrieving revision 1.18
  retrieving revision 1.19
  diff -u -r1.18 -r1.19
  --- Jar.java	2001/07/05 13:10:25	1.18
  +++ Jar.java	2001/07/28 14:11:41	1.19
  @@ -65,11 +65,10 @@
    * 
    * @author James Davidson <a href="mailto:duncan@x180.com">duncan@x180.com</a>
    */
  -
   public class Jar extends Zip {
   
  -    private File manifest;    
  -    private boolean manifestAdded;    
  +    private Manifest manifest;
  +    private Manifest execManifest;    
   
       public Jar() {
           super();
  @@ -83,16 +82,33 @@
       }
   
       public void setManifest(File manifestFile) {
  -        manifest = manifestFile;
  -        if (!manifest.exists())
  -            throw new BuildException("Manifest file: " + manifest + " does not exist.");
  -
  -        // Create a ZipFileSet for this file, and pass it up.
  -        ZipFileSet fs = new ZipFileSet();
  -        fs.setDir(new File(manifest.getParent()));
  -        fs.setIncludes(manifest.getName());
  -        fs.setFullpath("META-INF/MANIFEST.MF");
  -        super.addFileset(fs);
  +        if (!manifestFile.exists()) {
  +            throw new BuildException("Manifest file: " + manifestFile + " does not exist.",

  +                                     getLocation());
  +        }
  +        
  +        InputStream is = null;
  +        try {
  +            is = new FileInputStream(manifestFile);
  +            Manifest newManifest = new Manifest(is);
  +            if (manifest == null) {
  +                manifest = getDefaultManifest();
  +            }
  +            manifest.merge(newManifest);
  +        }
  +        catch (IOException e) {
  +            throw new BuildException("Unable to read manifest file: " + manifestFile, e);
  +        }
  +        finally {
  +            if (is != null) {
  +                try {
  +                    is.close();
  +                }
  +                catch (IOException e) {
  +                    // do nothing
  +                }
  +            }
  +        }
       }
   
       public void addMetainf(ZipFileSet fs) {
  @@ -106,43 +122,112 @@
       {
           // If no manifest is specified, add the default one.
           if (manifest == null) {
  -            String s = "/org/apache/tools/ant/defaultManifest.mf";
  -            InputStream in = this.getClass().getResourceAsStream(s);
  -            if ( in == null )
  -                throw new BuildException ( "Could not find: " + s );
  -            zipDir(null, zOut, "META-INF/");
  -            zipFile(in, zOut, "META-INF/MANIFEST.MF", System.currentTimeMillis());
  +            execManifest = null;
           }
  -
  +        else {
  +            execManifest = new Manifest();
  +            execManifest.merge(manifest);
  +        }
  +        zipDir(null, zOut, "META-INF/");
           super.initZipOutputStream(zOut);
       }
   
  +    private Manifest getDefaultManifest() throws IOException {
  +        String s = "/org/apache/tools/ant/defaultManifest.mf";
  +        InputStream in = this.getClass().getResourceAsStream(s);
  +        if (in == null) {
  +            throw new BuildException("Could not find: " + s);
  +        }
  +        return new Manifest(in);
  +    }   
  +    
  +    protected void finalizeZipOutputStream(ZipOutputStream zOut)
  +        throws IOException, BuildException {
  +
  +        if (execManifest == null) {
  +            execManifest = getDefaultManifest();
  +        }
  +        
  +        // time to write the manifest
  +        ByteArrayOutputStream baos = new ByteArrayOutputStream();
  +        PrintWriter writer = new PrintWriter(baos);
  +        execManifest.write(writer);
  +        writer.flush();
  +        
  +        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
  +        super.zipFile(bais, zOut, "META-INF/MANIFEST.MF", System.currentTimeMillis());
  +        super.finalizeZipOutputStream(zOut);
  +
  +    }
  +
  +    /**
  +     * Handle situation when we encounter a manifest file
  +     *
  +     * If we haven't been given one, we use this one.
  +     *
  +     * If we have, we merge the manifest in, provided it is a new file
  +     * and not the old one from the JAR we are updating
  +     */
  +    private void zipManifestEntry(InputStream is) throws IOException {
  +        if (execManifest == null) {
  +            execManifest = new Manifest(is);
  +        }
  +        else if (isAddingNewFiles()) {
  +            execManifest.merge(new Manifest(is));
  +        }
  +    }
  +    
       protected void zipFile(File file, ZipOutputStream zOut, String vPath)
           throws IOException
       {
  -        // If the file being added is META-INF/MANIFEST.MF, we warn if it's not the
  -        // one specified in the "manifest" attribute - or if it's being added twice, 
  -        // meaning the same file is specified by the "manifeset" attribute and in
  -        // a <fileset> element.
  +        // If the file being added is META-INF/MANIFEST.MF, we merge it with the
  +        // current manifest 
           if (vPath.equalsIgnoreCase("META-INF/MANIFEST.MF"))  {
  -            if (manifest == null || !manifest.equals(file) || manifestAdded) {
  -                log("Warning: selected "+archiveType+" files include a META-INF/MANIFEST.MF
which will be ignored " +
  -                    "(please use manifest attribute to "+archiveType+" task)", Project.MSG_WARN);
  -            } else {
  -                super.zipFile(file, zOut, vPath);
  -                manifestAdded = true;
  +            InputStream is = null;
  +            try {
  +                is = new FileInputStream(file);
  +                zipManifestEntry(is);
  +            }
  +            catch (IOException e) {
  +                throw new BuildException("Unable to read manifest file: " + file, e);
               }
  +            finally {
  +                if (is != null) {
  +                    try {
  +                        is.close();
  +                    }
  +                    catch (IOException e) {
  +                        // do nothing
  +                    }
  +                }
  +            }
           } else {
               super.zipFile(file, zOut, vPath);
           }
       }
   
  +    protected void zipFile(InputStream is, ZipOutputStream zOut, String vPath, long lastModified)
  +        throws IOException
  +    {
  +        // If the file being added is META-INF/MANIFEST.MF, we merge it with the
  +        // current manifest 
  +        if (vPath.equalsIgnoreCase("META-INF/MANIFEST.MF"))  {
  +            try {
  +                zipManifestEntry(is);
  +            }
  +            catch (IOException e) {
  +                throw new BuildException("Unable to read manifest file: ", e);
  +            }
  +        } else {
  +            super.zipFile(is, zOut, vPath, lastModified);
  +        }
  +    }
  +
       /**
        * Make sure we don't think we already have a MANIFEST next time this task
        * gets executed.
        */
       protected void cleanUp() {
  -        manifestAdded = false;
           super.cleanUp();
       }
   }
  
  
  
  1.43      +56 -31    jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Zip.java
  
  Index: Zip.java
  ===================================================================
  RCS file: /home/cvs/jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Zip.java,v
  retrieving revision 1.42
  retrieving revision 1.43
  diff -u -r1.42 -r1.43
  --- Zip.java	2001/07/12 10:24:46	1.42
  +++ Zip.java	2001/07/28 14:11:41	1.43
  @@ -87,7 +87,12 @@
       private Vector filesets = new Vector ();
       private Hashtable addedDirs = new Hashtable();
       private Vector addedFiles = new Vector();
  -
  +    
  +    /** true when we are adding new files into the Zip file, as opposed to 
  +        adding back the unchanged files */
  +    private boolean addingNewFiles;
  +    
  +    
       /**
        * Encoding to use for filenames, defaults to the platform's
        * default encoding.
  @@ -190,42 +195,46 @@
           }
   
           // Renamed version of original file, if it exists
  -        File renamedFile=null;
  +        File renamedFile = null;
           // Whether or not an actual update is required -
           // we don't need to update if the original file doesn't exist
  -        boolean reallyDoUpdate=false;
  +        
  +        addingNewFiles = true;
  +        boolean reallyDoUpdate = false;
           if (doUpdate && zipFile.exists())
           {
  -            reallyDoUpdate=true;
  +            reallyDoUpdate = true;
               
               int i;
               for (i=0; i < 1000; i++)
               {
  -                renamedFile = new File (zipFile.getParent(), "tmp."+i);
  +                renamedFile = new File(zipFile.getParent(), "tmp."+i);
                   
  -                if (!renamedFile.exists())
  +                if (!renamedFile.exists()) {
                       break;
  +                }
  +            }
  +            if (i == 1000) {
  +                throw new BuildException("Can't find available temporary filename to which
to rename old file.");
               }
  -            if (i==1000)
  -                throw new BuildException 
  -                ("Can't find temporary filename to rename old file to.");
  +            
               try
               {
  -                if (!zipFile.renameTo (renamedFile))
  -                    throw new BuildException 
  -                    ("Unable to rename old file to temporary file");
  +                if (!zipFile.renameTo(renamedFile)) {
  +                    throw new BuildException("Unable to rename old file to temporary file");
  +                }
               }
               catch (SecurityException e)
               {
  -                throw new BuildException 
  -                    ("Not allowed to rename old file to temporary file");
  +                throw new BuildException("Not allowed to rename old file to temporary file");
               }
           }
           
           // Create the scanners to pass to isUpToDate().
           Vector dss = new Vector ();
  -        if (baseDir != null)
  +        if (baseDir != null) {
               dss.addElement(getDirectoryScanner(baseDir));
  +        }
           for (int i=0; i<filesets.size(); i++) {
               FileSet fs = (FileSet) filesets.elementAt(i);
               dss.addElement (fs.getDirectoryScanner(project));
  @@ -236,9 +245,11 @@
   
           // quick exit if the target is up to date
           // can also handle empty archives
  -        if (isUpToDate(scanners, zipFile)) return;
  +        if (isUpToDate(scanners, zipFile)) {
  +            return;
  +        }
   
  -        String action=reallyDoUpdate ? "Updating " : "Building ";
  +        String action = reallyDoUpdate ? "Updating " : "Building ";
           
           log(action + archiveType +": "+ zipFile.getAbsolutePath());
   
  @@ -256,28 +267,30 @@
                   initZipOutputStream(zOut);
   
                   // Add the implicit fileset to the archive.
  -                if (baseDir != null)
  +                if (baseDir != null) {
                       addFiles(getDirectoryScanner(baseDir), zOut, "", "");
  +                }
                   // Add the explicit filesets to the archive.
                   addFiles(filesets, zOut);
  -                if (reallyDoUpdate)
  -                {
  -                    ZipFileSet oldFiles = new ZipFileSet ();
  -                    oldFiles.setSrc (renamedFile);
  +                if (reallyDoUpdate) {
  +                    addingNewFiles = false;
  +                    ZipFileSet oldFiles = new ZipFileSet();
  +                    oldFiles.setSrc(renamedFile);
                       
  -                    StringBuffer exclusionPattern=new StringBuffer();
  +                    StringBuffer exclusionPattern = new StringBuffer();
                       for (int i=0; i < addedFiles.size(); i++)
                       {
  -                        if (i != 0)
  -                            exclusionPattern.append (",");
  -                        exclusionPattern.append 
  -                            ((String) addedFiles.elementAt(i));
  +                        if (i != 0) {
  +                            exclusionPattern.append(",");
  +                        }
  +                        exclusionPattern.append((String)addedFiles.elementAt(i));
                       }
  -                    oldFiles.setExcludes (exclusionPattern.toString());
  +                    oldFiles.setExcludes(exclusionPattern.toString());
                       Vector tmp = new Vector();
  -                    tmp.addElement (oldFiles);
  -                    addFiles (tmp, zOut);
  +                    tmp.addElement(oldFiles);
  +                    addFiles(tmp, zOut);
                   }
  +                finalizeZipOutputStream(zOut);
                   success = true;
               } finally {
                   // Close the output stream.
  @@ -322,6 +335,14 @@
       }
   
       /**
  +     * Indicates if the task is adding new files into the archive as opposed to
  +     * copying back unchanged files from the backup copy
  +     */
  +    protected boolean isAddingNewFiles() {
  +        return addingNewFiles;
  +    }
  +    
  +    /**
        * Add all files of the given FileScanner to the ZipOutputStream
        * prependig the given prefix to each filename.
        *
  @@ -406,6 +427,10 @@
       {
       }
   
  +    protected void finalizeZipOutputStream(ZipOutputStream zOut)
  +        throws IOException, BuildException
  +    {
  +    }
       /**
        * Check whether the archive is up-to-date; and handle behavior for empty archives.
        * @param scanners list of prepared scanners containing files to archive
  @@ -589,7 +614,7 @@
               }
               count = in.read(buffer, 0, buffer.length);
           } while (count != -1);
  -        addedFiles.addElement (vPath);
  +        addedFiles.addElement(vPath);
       }
   
       protected void zipFile(File file, ZipOutputStream zOut, String vPath)
  
  
  
  1.1                  jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Manifest.java
  
  Index: Manifest.java
  ===================================================================
  /*
   * The Apache Software License, Version 1.1
   *
   * Copyright (c) 2001 The Apache Software Foundation.  All rights 
   * reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   *
   * 1. Redistributions of source code must retain the above copyright
   *    notice, this list of conditions and the following disclaimer. 
   *
   * 2. Redistributions in binary form must reproduce the above copyright
   *    notice, this list of conditions and the following disclaimer in
   *    the documentation and/or other materials provided with the
   *    distribution.
   *
   * 3. The end-user documentation included with the redistribution, if
   *    any, must include the following acknowlegement:  
   *       "This product includes software developed by the 
   *        Apache Software Foundation (http://www.apache.org/)."
   *    Alternately, this acknowlegement may appear in the software itself,
   *    if and wherever such third-party acknowlegements normally appear.
   *
   * 4. The names "The Jakarta Project", "Ant", and "Apache Software
   *    Foundation" must not be used to endorse or promote products derived
   *    from this software without prior written permission. For written 
   *    permission, please contact apache@apache.org.
   *
   * 5. Products derived from this software may not be called "Apache"
   *    nor may "Apache" appear in their names without prior written
   *    permission of the Apache Group.
   *
   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   * SUCH DAMAGE.
   * ====================================================================
   *
   * This software consists of voluntary contributions made by many
   * individuals on behalf of the Apache Software Foundation.  For more
   * information on the Apache Software Foundation, please see
   * <http://www.apache.org/>.
   */
  
  package org.apache.tools.ant.taskdefs;
  
  import java.util.*;
  import java.io.*;
  
  /**
   * Class to manage Manifest information
   * 
   * @author <a href="mailto:conor@apache.org">Conor MacNeill</a>
   */
  public class Manifest {
      static public final String ATTR_MANIFEST_VERSION = "Manifest-Version";
      static public final String ATTR_SIGNATURE_VERSION = "Signature-Version";
      static public final String ATTR_NAME = "Name";
      static public final String ATTR_FROM = "From";
      static public final String DEFAULT_MANIFEST_VERSION = "1.0";
      static public final int MAX_LINE_LENGTH = 70;
      
      /**
       * Class to hold manifest attributes
       */
      static private class Attribute {
          /** The attribute's name */
          private String name = null;
          
          /** The attribute's value */
          private String value = null;
  
          public Attribute() {
          }
          
          public Attribute(String line) throws IOException {
              parse(line);
          }
          
          public Attribute(String name, String value) {
              this.name = name;
              this.value = value;
          }
          
          public void parse(String line) throws IOException {
              int index = line.indexOf(": ");
              if (index == -1) {
                  throw new IOException("Manifest line \"" + line + "\" is not valid");
              }
              name = line.substring(0, index);
              value = line.substring(index + 2);
          } 
          
          public void setName(String name) {
              this.name = name;
          }
          
          public String getName() {
              return name;
          }
          
          public void setValue(String value) {
              this.value = value;
          }
          
          public String getValue() {
              return value;
          }
          
          public void addContinuation(String line) {
              value += line.substring(1);
          }
          
          public void write(PrintWriter writer) throws IOException {
              String line = name + ": " + value;
              while (line.getBytes().length > MAX_LINE_LENGTH) {
                  // try to find a MAX_LINE_LENGTH byte section
                  int breakIndex = MAX_LINE_LENGTH;
                  String section = line.substring(0, breakIndex);
                  while (section.getBytes().length > MAX_LINE_LENGTH && breakIndex
> 0) {
                      breakIndex--;
                      section = line.substring(0, breakIndex);
                  }
                  if (breakIndex == 0) {
                      throw new IOException("Unable to write manifest line " + name + ": "
+ value);
                  }
                  writer.println(section);
                  line = " " + line.substring(breakIndex);
              }
              writer.println(line);
          }    
      }
  
      /** 
       * Class to represent an individual section in the 
       * Manifest 
       */
      static private class Section {
          private String name = null;
          
          private Hashtable attributes = new Hashtable();
          
          public void setName(String name) {
              this.name = name;
          }
          
          public String getName() {
              return name;
          }
          
          public void read(BufferedReader reader) throws IOException {
              Attribute attribute = null;
              while (true) { 
                  String line = reader.readLine();
                  if (line == null || line.length() == 0) {
                      return;
                  }
                  if (line.charAt(0) == ' ') {
                      // continuation line
                      if (attribute == null) {
                          throw new IOException("Can't start an attribute with a continuation
line " + line);
                      }
                      attribute.addContinuation(line);
                  }
                  else {
                      attribute = new Attribute(line);
                      if (name == null && attribute.getName().equalsIgnoreCase(ATTR_NAME))
{
                          throw new IOException("The " + ATTR_NAME + " header may not occur
in the main section ");
                      }
                      
                      if (attribute.getName().toLowerCase().startsWith(ATTR_FROM.toLowerCase()))
{
                          throw new IOException("Attribute names may not start with " + ATTR_FROM);
                      }
                      
                      addAttribute(attribute);
                  }
              }
          }
          
          public void merge(Section section) throws IOException {
              if (name == null && section.getName() != null ||
                      name != null && !(name.equalsIgnoreCase(section.getName())))
{
                  throw new IOException("Unable to merge sections with different names");
              }
              
              for (Enumeration e = section.attributes.keys(); e.hasMoreElements();) {
                  String attributeName = (String)e.nextElement();
                  // the merge file always wins
                  attributes.put(attributeName, section.attributes.get(attributeName));
              }
          }
          
          public void write(PrintWriter writer) throws IOException {
              if (name != null) {
                  Attribute nameAttr = new Attribute(ATTR_NAME, name);
                  nameAttr.write(writer);
              }
              for (Enumeration e = attributes.elements(); e.hasMoreElements();) {
                  Attribute attribute = (Attribute)e.nextElement();
                  attribute.write(writer);
              }
              writer.println();
          }
      
          public String getAttributeValue(String attributeName) {
              Attribute attribute = (Attribute)attributes.get(attributeName.toLowerCase());
              if (attribute == null) {
                  return null;
              }
              return attribute.getValue();
          }
          
          public void removeAttribute(String attributeName) {
              attributes.remove(attributeName.toLowerCase());
          }
          
          public void addAttribute(Attribute attribute) throws IOException {
              if (attributes.containsKey(attribute.getName().toLowerCase())) {
                  throw new IOException("The attribute \"" + attribute.getName() + "\" may
not occur more than" +
                                        " once in the same section");
              }
              attributes.put(attribute.getName().toLowerCase(), attribute);
          }
      }
  
      
      private String manifestVersion = DEFAULT_MANIFEST_VERSION;
      private Section mainSection = new Section();
      private Hashtable sections = new Hashtable();
  
      public Manifest() {
      }
      
      /**
       * Read a manifest file from the given input stream
       *
       * @param is the input stream from which the Manifest is read 
       */
      public Manifest(InputStream is) throws IOException {
          BufferedReader reader = new BufferedReader(new InputStreamReader(is));
          String line = reader.readLine();
          if (line == null) {
              return;
          }
          
          // This should be the manifest version
          Attribute version = new Attribute(line);
          if (!version.getName().equalsIgnoreCase(ATTR_MANIFEST_VERSION)) {
              throw new IOException("Manifest must start with \"" + ATTR_MANIFEST_VERSION
+ 
                                    "\" and not \"" + line + "\"");
          }
          manifestVersion = version.getValue();
          mainSection.read(reader);
          
          while ((line = reader.readLine()) != null) {
              if (line.length() == 0) {
                  continue;
              }
              Attribute sectionName = new Attribute(line);
              if (!sectionName.getName().equalsIgnoreCase(ATTR_NAME)) {
                  throw new IOException("Manifest sections should start with a \"" + ATTR_NAME
+ 
                                        "\" attribute and not \"" + sectionName.getName()
+ "\"");
              }
                  
              Section section = new Section();
              section.setName(sectionName.getValue());
              section.read(reader);
              sections.put(section.getName().toLowerCase(), section);
          }
      }
      
      /**
       * Merge the contents of the given manifest into this manifest
       */
      public void merge(Manifest other) throws IOException {
          manifestVersion = other.manifestVersion;
          mainSection.merge(other.mainSection);
          for (Enumeration e = other.sections.keys(); e.hasMoreElements();) {
              String sectionName = (String)e.nextElement();
              Section ourSection = (Section)sections.get(sectionName);
              Section otherSection = (Section)other.sections.get(sectionName);
              if (ourSection == null) {
                  sections.put(sectionName.toLowerCase(), otherSection);
              }
              else {
                  ourSection.merge(otherSection);
              }
          }
      }
      
      public void write(PrintWriter writer) throws IOException {
          writer.println(ATTR_MANIFEST_VERSION + ": " + manifestVersion);
          String signatureVersion = mainSection.getAttributeValue(ATTR_SIGNATURE_VERSION);
          if (signatureVersion != null) {
              writer.println(ATTR_SIGNATURE_VERSION + ": " + signatureVersion);
              mainSection.removeAttribute(ATTR_SIGNATURE_VERSION);
          }
          mainSection.write(writer);
          if (signatureVersion != null) {
              mainSection.addAttribute(new Attribute(ATTR_SIGNATURE_VERSION, signatureVersion));
          }
          
          for (Enumeration e = sections.elements(); e.hasMoreElements();) {
              Section section = (Section)e.nextElement();
              section.write(writer);
          }
      }
      
      public String toString() {
          StringWriter sw = new StringWriter();
          try {
              write(new PrintWriter(sw));
          }
          catch (IOException e) {
              return null;
          }
          return sw.toString();
      }
  }
  
  
  

Mime
View raw message